2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mielecloud.internal.handler;
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.ArgumentMatchers.*;
17 import static org.mockito.Mockito.*;
18 import static org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants.Channels.*;
19 import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.*;
20 import static org.openhab.binding.mielecloud.internal.util.ReflectionUtil.setPrivate;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.List;
25 import java.util.Objects;
26 import java.util.Optional;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.junit.jupiter.api.BeforeEach;
31 import org.junit.jupiter.api.Test;
32 import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
33 import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
34 import org.openhab.binding.mielecloud.internal.auth.OpenHabOAuthTokenRefresher;
35 import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
36 import org.openhab.binding.mielecloud.internal.util.ReflectionUtil;
37 import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
38 import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
39 import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
40 import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
41 import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
42 import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
43 import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
44 import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
45 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
46 import org.openhab.core.auth.client.oauth2.OAuthClientService;
47 import org.openhab.core.auth.client.oauth2.OAuthFactory;
48 import org.openhab.core.config.core.Configuration;
49 import org.openhab.core.items.Item;
50 import org.openhab.core.items.ItemBuilder;
51 import org.openhab.core.items.ItemBuilderFactory;
52 import org.openhab.core.items.ItemRegistry;
53 import org.openhab.core.library.types.DecimalType;
54 import org.openhab.core.library.types.OnOffType;
55 import org.openhab.core.library.types.StringType;
56 import org.openhab.core.test.java.JavaOSGiTest;
57 import org.openhab.core.thing.Bridge;
58 import org.openhab.core.thing.Channel;
59 import org.openhab.core.thing.ChannelUID;
60 import org.openhab.core.thing.Thing;
61 import org.openhab.core.thing.ThingRegistry;
62 import org.openhab.core.thing.ThingStatus;
63 import org.openhab.core.thing.ThingStatusDetail;
64 import org.openhab.core.thing.ThingTypeUID;
65 import org.openhab.core.thing.ThingUID;
66 import org.openhab.core.thing.binding.ThingHandler;
67 import org.openhab.core.thing.binding.ThingHandlerFactory;
68 import org.openhab.core.thing.binding.builder.BridgeBuilder;
69 import org.openhab.core.thing.binding.builder.ChannelBuilder;
70 import org.openhab.core.thing.binding.builder.ThingBuilder;
71 import org.openhab.core.thing.link.ItemChannelLink;
72 import org.openhab.core.thing.link.ItemChannelLinkRegistry;
73 import org.openhab.core.thing.type.ChannelDefinition;
74 import org.openhab.core.thing.type.ChannelType;
75 import org.openhab.core.thing.type.ChannelTypeRegistry;
76 import org.openhab.core.thing.type.ChannelTypeUID;
77 import org.openhab.core.thing.type.ThingType;
78 import org.openhab.core.thing.type.ThingTypeRegistry;
79 import org.openhab.core.types.State;
80 import org.openhab.core.types.UnDefType;
83 * @author Björn Lange - Initial contribution
86 public abstract class AbstractMieleThingHandlerTest extends JavaOSGiTest {
87 protected static final State NULL_VALUE_STATE = UnDefType.UNDEF;
90 private Bridge bridge;
92 private MieleBridgeHandler bridgeHandler;
94 private ThingRegistry thingRegistry;
96 private MieleWebservice webserviceMock;
98 private AbstractMieleThingHandler thingHandler;
101 private ItemRegistry itemRegistry;
103 protected Bridge getBridge() {
104 assertNotNull(bridge);
105 return Objects.requireNonNull(bridge);
108 protected MieleBridgeHandler getBridgeHandler() {
109 assertNotNull(bridgeHandler);
110 return Objects.requireNonNull(bridgeHandler);
113 protected ThingRegistry getThingRegistry() {
114 assertNotNull(thingRegistry);
115 return Objects.requireNonNull(thingRegistry);
118 protected MieleWebservice getWebserviceMock() {
119 assertNotNull(webserviceMock);
120 return Objects.requireNonNull(webserviceMock);
123 protected AbstractMieleThingHandler getThingHandler() {
124 assertNotNull(thingHandler);
125 return Objects.requireNonNull(thingHandler);
128 protected ItemRegistry getItemRegistry() {
129 assertNotNull(itemRegistry);
130 return Objects.requireNonNull(itemRegistry);
134 protected void waitForAssert(Runnable assertion) {
135 // Use a larger timeout to avoid test failures in the CI build.
136 waitForAssert(assertion, 2 * DFL_TIMEOUT, 2 * DFL_SLEEP_TIME);
139 private void setUpThingRegistry() {
140 thingRegistry = getService(ThingRegistry.class, ThingRegistry.class);
141 assertNotNull(thingRegistry, "Thing registry is missing");
144 private void setUpItemRegistry() {
145 itemRegistry = getService(ItemRegistry.class, ItemRegistry.class);
146 assertNotNull(itemRegistry);
149 private void setUpWebservice()
150 throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
151 webserviceMock = mock(MieleWebservice.class);
152 when(getWebserviceMock().hasAccessToken()).thenReturn(true);
154 MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
155 when(webserviceFactory.create(any())).thenReturn(getWebserviceMock());
157 MieleHandlerFactory factory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
158 assertNotNull(factory);
159 setPrivate(Objects.requireNonNull(factory), "webserviceFactory", webserviceFactory);
162 private void setUpBridge() throws Exception {
163 AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
164 accessTokenResponse.setAccessToken(ACCESS_TOKEN);
166 OAuthClientService oAuthClientService = mock(OAuthClientService.class);
167 when(oAuthClientService.getAccessTokenResponse()).thenReturn(accessTokenResponse);
169 OAuthFactory oAuthFactory = mock(OAuthFactory.class);
171 .getOAuthClientService(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString()))
172 .thenReturn(oAuthClientService);
174 OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
175 OpenHabOAuthTokenRefresher.class);
176 assertNotNull(tokenRefresher);
177 setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
179 bridge = BridgeBuilder
180 .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
181 MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
182 .withLabel("Miele@home Account")
184 new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
185 MieleCloudBindingIntegrationTestConstants.EMAIL)))
187 assertNotNull(bridge);
189 getThingRegistry().add(getBridge());
192 waitForAssert(() -> {
193 assertNotNull(getBridge().getHandler());
194 assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
197 MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) getBridge().getHandler();
198 assertNotNull(bridgeHandler);
200 waitForAssert(() -> {
201 assertNotNull(bridgeHandler.getThing());
204 bridgeHandler.initialize();
205 bridgeHandler.onConnectionAlive();
206 setPrivate(bridgeHandler, "discoveryService", null);
207 this.bridgeHandler = bridgeHandler;
210 protected AbstractMieleThingHandler createThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid,
211 Class<? extends AbstractMieleThingHandler> expectedHandlerClass, String deviceIdentifier) {
212 ThingRegistry registry = getThingRegistry();
214 List<Channel> channels = createChannelsForThingHandler(thingTypeUid, thingUid);
216 Thing thing = ThingBuilder.create(thingTypeUid, thingUid)
217 .withConfiguration(new Configuration(Collections
218 .singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, deviceIdentifier)))
219 .withBridge(getBridge().getUID()).withChannels(channels).withLabel("DA-6996").build();
220 assertNotNull(thing);
224 waitForAssert(() -> {
225 ThingHandler handler = thing.getHandler();
226 assertNotNull(handler);
227 assertTrue(expectedHandlerClass.isAssignableFrom(handler.getClass()), "Handler type is wrong");
230 createItemsForChannels(thing);
231 linkChannelsToItems(thing);
233 ThingHandler handler = thing.getHandler();
234 assertNotNull(handler);
235 AbstractMieleThingHandler mieleThingHandler = (AbstractMieleThingHandler) Objects.requireNonNull(handler);
237 waitForAssert(() -> {
239 assertNotNull(ReflectionUtil.invokePrivate(mieleThingHandler, "getBridge"));
240 } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) {
241 throw new RuntimeException(e);
243 assertNotNull(getBridge().getThing(thingUid));
246 return mieleThingHandler;
249 private List<Channel> createChannelsForThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid) {
250 ChannelTypeRegistry channelTypeRegistry = getService(ChannelTypeRegistry.class, ChannelTypeRegistry.class);
251 assertNotNull(channelTypeRegistry);
253 ThingTypeRegistry thingTypeRegistry = getService(ThingTypeRegistry.class, ThingTypeRegistry.class);
254 assertNotNull(thingTypeRegistry);
256 ThingType thingType = thingTypeRegistry.getThingType(thingTypeUid);
257 assertNotNull(thingType);
259 List<ChannelDefinition> channelDefinitions = thingType.getChannelDefinitions();
260 assertNotNull(channelDefinitions);
262 List<Channel> channels = new ArrayList<Channel>();
263 for (ChannelDefinition channelDefinition : channelDefinitions) {
264 ChannelTypeUID channelTypeUid = channelDefinition.getChannelTypeUID();
265 assertNotNull(channelTypeUid);
267 ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUid);
268 assertNotNull(channelType);
270 String acceptedItemType = channelType.getItemType();
271 assertNotNull(acceptedItemType);
273 String channelId = channelDefinition.getId();
274 assertNotNull(channelId);
276 ChannelUID channelUid = new ChannelUID(thingUid, channelId);
277 assertNotNull(channelUid);
279 Channel channel = ChannelBuilder.create(channelUid, acceptedItemType).build();
280 assertNotNull(channel);
282 channels.add(channel);
288 private void createItemsForChannels(Thing thing) {
289 ItemBuilderFactory itemBuilderFactory = getService(ItemBuilderFactory.class);
290 assertNotNull(itemBuilderFactory);
292 for (Channel channel : thing.getChannels()) {
293 String acceptedItemType = channel.getAcceptedItemType();
294 assertNotNull(acceptedItemType);
296 ItemBuilder itemBuilder = itemBuilderFactory.newItemBuilder(Objects.requireNonNull(acceptedItemType),
297 channel.getUID().getId());
298 assertNotNull(itemBuilder);
300 Item item = itemBuilder.build();
303 getItemRegistry().add(item);
307 private void linkChannelsToItems(Thing thing) {
308 ItemChannelLinkRegistry itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class,
309 ItemChannelLinkRegistry.class);
310 assertNotNull(itemChannelLinkRegistry);
312 for (Channel channel : thing.getChannels()) {
313 String itemName = channel.getUID().getId();
314 assertNotNull(itemName);
316 ChannelUID channelUid = channel.getUID();
317 assertNotNull(channelUid);
319 ItemChannelLink link = itemChannelLinkRegistry.add(new ItemChannelLink(itemName, channelUid));
324 protected ChannelUID channel(String id) {
325 return new ChannelUID(getThingHandler().getThing().getUID(), id);
329 public void setUpAbstractMieleThingHandlerTest() throws Exception {
330 registerVolatileStorageService();
331 setUpThingRegistry();
336 protected void setUpBridgeAndThing() throws Exception {
338 thingHandler = setUpThingHandler();
341 private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
342 assertThingStatusIs(thing, expectedStatus, expectedStatusDetail, null);
345 private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
346 @Nullable String expectedDescription) {
347 assertEquals(expectedStatus, thing.getStatus());
348 assertEquals(expectedStatusDetail, thing.getStatusInfo().getStatusDetail());
349 if (expectedDescription == null) {
350 assertNull(thing.getStatusInfo().getDescription());
352 assertEquals(expectedDescription, thing.getStatusInfo().getDescription());
356 protected State getChannelState(String channelUid) {
357 Item item = getItemRegistry().get(channelUid);
358 assertNotNull(item, "Item for channel UID " + channelUid + " is null.");
359 return item.getState();
363 * Sets up the {@link ThingHandler} under test.
365 * @return The created {@link ThingHandler}.
367 protected abstract AbstractMieleThingHandler setUpThingHandler();
370 public void testCachedStateIsQueriedOnInitialize() throws Exception {
372 setUpBridgeAndThing();
375 verify(getWebserviceMock()).dispatchDeviceState(SERIAL_NUMBER);
379 public void testThingStatusIsOfflineWithDetailGoneAndDetailMessageWhenDeviceIsRemoved() throws Exception {
381 setUpBridgeAndThing();
384 getBridgeHandler().onDeviceRemoved(SERIAL_NUMBER);
387 Thing thing = getThingHandler().getThing();
388 assertThingStatusIs(thing, ThingStatus.OFFLINE, ThingStatusDetail.GONE,
389 "@text/mielecloud.thing.status.removed");
392 private DeviceState createDeviceStateMock(StateType stateType, String localizedState) {
393 DeviceState deviceState = mock(DeviceState.class);
394 when(deviceState.getDeviceIdentifier()).thenReturn(getThingHandler().getThing().getUID().getId());
395 when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
396 when(deviceState.getStateType()).thenReturn(Optional.of(stateType));
397 when(deviceState.isInState(any())).thenCallRealMethod();
398 when(deviceState.getStatus()).thenReturn(Optional.of(localizedState));
403 public void testStatusIsSetToOnlineWhenDeviceStateIsValid() throws Exception {
405 setUpBridgeAndThing();
407 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
410 getBridgeHandler().onDeviceStateUpdated(deviceState);
413 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
417 public void testStatusIsSetToOfflineWhenDeviceIsNotConnected() throws Exception {
419 setUpBridgeAndThing();
421 DeviceState deviceState = createDeviceStateMock(StateType.NOT_CONNECTED, "Not connected");
424 getBridgeHandler().onDeviceStateUpdated(deviceState);
427 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
428 "@text/mielecloud.thing.status.disconnected");
432 public void testFailingPutProcessActionDoesNotSetTheDeviceToOffline() throws Exception {
434 doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putProcessAction(any(),
435 eq(ProcessAction.STOP));
437 setUpBridgeAndThing();
439 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
440 getBridgeHandler().onDeviceStateUpdated(deviceState);
441 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
444 getThingHandler().triggerProcessAction(ProcessAction.STOP);
447 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
451 public void testHandleCommandProgramStartToStartStopChannel() throws Exception {
453 setUpBridgeAndThing();
456 getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
457 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
460 waitForAssert(() -> {
461 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
466 public void testHandleCommandProgramStopToStartStopChannel() throws Exception {
468 setUpBridgeAndThing();
471 getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
472 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
475 waitForAssert(() -> {
476 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
481 public void testHandleCommandProgramStartToStartStopPauseChannel() throws Exception {
483 setUpBridgeAndThing();
486 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
487 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
490 waitForAssert(() -> {
491 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
496 public void testHandleCommandProgramStopToStartStopPauseChannel() throws Exception {
498 setUpBridgeAndThing();
501 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
502 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
505 waitForAssert(() -> {
506 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
511 public void testHandleCommandProgramPauseToStartStopPauseChannel() throws Exception {
513 setUpBridgeAndThing();
516 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
517 new StringType(ProgramStatus.PROGRAM_PAUSED.getState()));
520 waitForAssert(() -> {
521 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.PAUSE);
526 public void testFailingPutLightDoesNotSetTheDeviceToOffline() throws Exception {
528 doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putLight(any(), eq(true));
530 setUpBridgeAndThing();
532 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
533 getBridgeHandler().onDeviceStateUpdated(deviceState);
534 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
537 getThingHandler().triggerLight(true);
540 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
544 public void testHandleCommandLightOff() throws Exception {
546 setUpBridgeAndThing();
549 getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.OFF);
552 waitForAssert(() -> {
553 verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), false);
558 public void testHandleCommandLightOn() throws Exception {
560 setUpBridgeAndThing();
563 getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.ON);
566 waitForAssert(() -> {
567 verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), true);
572 public void testHandleCommandDoesNothingWhenCommandIsNotOfOnOffType() throws Exception {
574 setUpBridgeAndThing();
577 getThingHandler().handleCommand(channel(LIGHT_SWITCH), new DecimalType(0));
580 verify(getWebserviceMock(), never()).putLight(anyString(), anyBoolean());
584 public void testHandleCommandPowerOn() throws Exception {
586 setUpBridgeAndThing();
589 getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.ON);
592 waitForAssert(() -> {
593 verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), true);
598 public void testHandleCommandPowerOff() throws Exception {
600 setUpBridgeAndThing();
603 getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.OFF);
606 waitForAssert(() -> {
607 verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), false);
612 public void testHandleCommandDoesNothingWhenPowerCommandIsNotOfOnOffType() throws Exception {
614 setUpBridgeAndThing();
617 getThingHandler().handleCommand(channel(POWER_ON_OFF), new DecimalType(0));
620 verify(getWebserviceMock(), never()).putPowerState(anyString(), anyBoolean());
624 public void testMissingPropertiesAreSetWhenAStateUpdateIsReceivedFromTheCloud() throws Exception {
626 setUpBridgeAndThing();
628 assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_SERIAL_NUMBER));
629 assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_MODEL_ID));
631 var deviceState = mock(DeviceState.class);
632 when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
633 when(deviceState.getDeviceIdentifier()).thenReturn(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
634 when(deviceState.getFabNumber())
635 .thenReturn(Optional.of(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER));
636 when(deviceState.getType()).thenReturn(Optional.of("Unknown device type"));
637 when(deviceState.getTechType()).thenReturn(Optional.of("UK-4567"));
640 getThingHandler().onDeviceStateUpdated(deviceState);
643 assertEquals(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER,
644 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_SERIAL_NUMBER));
645 assertEquals("Unknown device type UK-4567",
646 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_MODEL_ID));