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.TransitionChannelState;
-import org.openhab.binding.mielecloud.internal.webservice.ActionStateFetcher;
import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
import org.openhab.binding.mielecloud.internal.webservice.UnavailableMieleWebservice;
import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
*/
@NonNullByDefault
public abstract class AbstractMieleThingHandler extends BaseThingHandler {
- protected final ActionStateFetcher actionFetcher;
protected DeviceState latestDeviceState = new DeviceState(getDeviceId(), null);
protected TransitionState latestTransitionState = new TransitionState(null, latestDeviceState);
protected ActionsState latestActionsState = new ActionsState(getDeviceId(), null);
*/
public AbstractMieleThingHandler(Thing thing) {
super(thing);
- this.actionFetcher = new ActionStateFetcher(this::getWebservice, scheduler);
}
private Optional<MieleBridgeHandler> getMieleBridgeHandler() {
* 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) {
- actionFetcher.onDeviceStateUpdated(deviceState);
-
latestTransitionState = new TransitionState(latestTransitionState, deviceState);
latestDeviceState = deviceState;
*/
package org.openhab.binding.mielecloud.internal.handler.channel;
+import java.math.BigDecimal;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
* Converts an {@link Optional} of {@link Integer} to {@link State}.
*/
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}.
*/
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);
}
/**
+++ /dev/null
-/**
- * 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());
- }
- }
-}
import org.eclipse.jetty.client.api.ContentResponse;
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.ActionsCollection;
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.MieleSyntaxException;
private static final String ENDPOINT_ALL_SSE_EVENTS = ENDPOINT_DEVICES + "all/events";
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();
public void onServerSentEvent(ServerSentEvent event) {
fireConnectionAlive();
- if (!SSE_EVENT_TYPE_DEVICES.equals(event.getEvent())) {
- return;
- }
-
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) {
logger.warn("SSE payload is not valid Json: {}", event.getData());
}
--- /dev/null
+/**
+ * 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 + "]";
+ }
+}
private final String event;
private final String data;
- ServerSentEvent(String event, String data) {
+ public ServerSentEvent(String event, String data) {
this.event = event;
this.data = data;
}
+++ /dev/null
-/**
- * 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());
- }
-}
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.RetryStrategyCombiner;
+import org.openhab.binding.mielecloud.internal.webservice.sse.ServerSentEvent;
import org.openhab.core.io.net.http.HttpClientFactory;
/**
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_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 ACCESS_TOKEN = "DE_0123456789abcdef0123456789abcdef";
}
}
+ @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.
*
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
+import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
// when:
boolean canBeStarted = actionState.canBeStarted();
+ boolean canBeStopped = actionState.canBeStopped();
// then:
assertTrue(canBeStarted);
+ assertFalse(canBeStopped);
}
@Test
when(actions.getProcessAction()).thenReturn(Collections.singletonList(ProcessAction.STOP));
// when:
+ boolean canBeStarted = actionState.canBeStarted();
+ boolean canBeStopped = actionState.canBeStopped();
+
+ // 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);
}
--- /dev/null
+/**
+ * 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());
+ }
+}
import java.io.IOException;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
// then:
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());
+ }
}
--- /dev/null
+{
+ "000123456789": {
+ "processAction": [1, 2],
+ "light": [2],
+ "ambientLight": [],
+ "startTime": null,
+ "ventilationStep": [],
+ "programId": [123],
+ "targetTemperature": [],
+ "deviceName": false,
+ "powerOn": true,
+ "powerOff": false,
+ "colors": [],
+ "modes": []
+ }
+}
\ No newline at end of file