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.webservice.MieleWebservice;
37 import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
38 import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
39 import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
40 import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
41 import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
42 import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
43 import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
44 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
45 import org.openhab.core.auth.client.oauth2.OAuthClientService;
46 import org.openhab.core.auth.client.oauth2.OAuthFactory;
47 import org.openhab.core.config.core.Configuration;
48 import org.openhab.core.items.Item;
49 import org.openhab.core.items.ItemBuilder;
50 import org.openhab.core.items.ItemBuilderFactory;
51 import org.openhab.core.items.ItemRegistry;
52 import org.openhab.core.library.types.DecimalType;
53 import org.openhab.core.library.types.OnOffType;
54 import org.openhab.core.library.types.StringType;
55 import org.openhab.core.test.java.JavaOSGiTest;
56 import org.openhab.core.thing.Bridge;
57 import org.openhab.core.thing.Channel;
58 import org.openhab.core.thing.ChannelUID;
59 import org.openhab.core.thing.Thing;
60 import org.openhab.core.thing.ThingRegistry;
61 import org.openhab.core.thing.ThingStatus;
62 import org.openhab.core.thing.ThingStatusDetail;
63 import org.openhab.core.thing.ThingTypeUID;
64 import org.openhab.core.thing.ThingUID;
65 import org.openhab.core.thing.binding.ThingHandler;
66 import org.openhab.core.thing.binding.ThingHandlerFactory;
67 import org.openhab.core.thing.binding.builder.BridgeBuilder;
68 import org.openhab.core.thing.binding.builder.ChannelBuilder;
69 import org.openhab.core.thing.binding.builder.ThingBuilder;
70 import org.openhab.core.thing.link.ItemChannelLink;
71 import org.openhab.core.thing.link.ItemChannelLinkRegistry;
72 import org.openhab.core.thing.type.ChannelDefinition;
73 import org.openhab.core.thing.type.ChannelType;
74 import org.openhab.core.thing.type.ChannelTypeRegistry;
75 import org.openhab.core.thing.type.ChannelTypeUID;
76 import org.openhab.core.thing.type.ThingType;
77 import org.openhab.core.thing.type.ThingTypeRegistry;
78 import org.openhab.core.types.State;
79 import org.openhab.core.types.UnDefType;
82 * @author Björn Lange - Initial contribution
85 public abstract class AbstractMieleThingHandlerTest extends JavaOSGiTest {
86 protected static final State NULL_VALUE_STATE = UnDefType.UNDEF;
89 private Bridge bridge;
91 private MieleBridgeHandler bridgeHandler;
93 private ThingRegistry thingRegistry;
95 private MieleWebservice webserviceMock;
97 private AbstractMieleThingHandler thingHandler;
100 private ItemRegistry itemRegistry;
102 protected Bridge getBridge() {
103 assertNotNull(bridge);
104 return Objects.requireNonNull(bridge);
107 protected MieleBridgeHandler getBridgeHandler() {
108 assertNotNull(bridgeHandler);
109 return Objects.requireNonNull(bridgeHandler);
112 protected ThingRegistry getThingRegistry() {
113 assertNotNull(thingRegistry);
114 return Objects.requireNonNull(thingRegistry);
117 protected MieleWebservice getWebserviceMock() {
118 assertNotNull(webserviceMock);
119 return Objects.requireNonNull(webserviceMock);
122 protected AbstractMieleThingHandler getThingHandler() {
123 assertNotNull(thingHandler);
124 return Objects.requireNonNull(thingHandler);
127 protected ItemRegistry getItemRegistry() {
128 assertNotNull(itemRegistry);
129 return Objects.requireNonNull(itemRegistry);
133 protected void waitForAssert(Runnable assertion) {
134 // Use a larger timeout to avoid test failures in the CI build.
135 waitForAssert(assertion, 2 * DFL_TIMEOUT, 2 * DFL_SLEEP_TIME);
138 private void setUpThingRegistry() {
139 thingRegistry = getService(ThingRegistry.class, ThingRegistry.class);
140 assertNotNull(thingRegistry, "Thing registry is missing");
143 private void setUpItemRegistry() {
144 itemRegistry = getService(ItemRegistry.class, ItemRegistry.class);
145 assertNotNull(itemRegistry);
148 private void setUpWebservice()
149 throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
150 webserviceMock = mock(MieleWebservice.class);
151 when(getWebserviceMock().hasAccessToken()).thenReturn(true);
153 MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
154 when(webserviceFactory.create(any())).thenReturn(getWebserviceMock());
156 MieleHandlerFactory factory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
157 assertNotNull(factory);
158 setPrivate(Objects.requireNonNull(factory), "webserviceFactory", webserviceFactory);
161 private void setUpBridge() throws Exception {
162 AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
163 accessTokenResponse.setAccessToken(ACCESS_TOKEN);
165 OAuthClientService oAuthClientService = mock(OAuthClientService.class);
166 when(oAuthClientService.getAccessTokenResponse()).thenReturn(accessTokenResponse);
168 OAuthFactory oAuthFactory = mock(OAuthFactory.class);
170 .getOAuthClientService(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString()))
171 .thenReturn(oAuthClientService);
173 OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
174 OpenHabOAuthTokenRefresher.class);
175 assertNotNull(tokenRefresher);
176 setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
178 bridge = BridgeBuilder
179 .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
180 MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
181 .withLabel("Miele@home Account")
183 new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
184 MieleCloudBindingIntegrationTestConstants.EMAIL)))
186 assertNotNull(bridge);
188 getThingRegistry().add(getBridge());
191 waitForAssert(() -> {
192 assertNotNull(getBridge().getHandler());
193 assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
196 MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) getBridge().getHandler();
197 assertNotNull(bridgeHandler);
199 waitForAssert(() -> {
200 assertNotNull(bridgeHandler.getThing());
203 bridgeHandler.initialize();
204 bridgeHandler.onConnectionAlive();
205 setPrivate(bridgeHandler, "discoveryService", null);
206 this.bridgeHandler = bridgeHandler;
209 protected AbstractMieleThingHandler createThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid,
210 Class<? extends AbstractMieleThingHandler> expectedHandlerClass, String deviceIdentifier) {
211 ThingRegistry registry = getThingRegistry();
213 List<Channel> channels = createChannelsForThingHandler(thingTypeUid, thingUid);
215 Thing thing = ThingBuilder.create(thingTypeUid, thingUid)
216 .withConfiguration(new Configuration(Collections
217 .singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, deviceIdentifier)))
218 .withBridge(getBridge().getUID()).withChannels(channels).withLabel("DA-6996").build();
219 assertNotNull(thing);
223 waitForAssert(() -> {
224 ThingHandler handler = thing.getHandler();
225 assertNotNull(handler);
226 assertTrue(expectedHandlerClass.isAssignableFrom(handler.getClass()), "Handler type is wrong");
229 createItemsForChannels(thing);
230 linkChannelsToItems(thing);
232 ThingHandler handler = thing.getHandler();
233 assertNotNull(handler);
234 return (AbstractMieleThingHandler) Objects.requireNonNull(handler);
237 private List<Channel> createChannelsForThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid) {
238 ChannelTypeRegistry channelTypeRegistry = getService(ChannelTypeRegistry.class, ChannelTypeRegistry.class);
239 assertNotNull(channelTypeRegistry);
241 ThingTypeRegistry thingTypeRegistry = getService(ThingTypeRegistry.class, ThingTypeRegistry.class);
242 assertNotNull(thingTypeRegistry);
244 ThingType thingType = thingTypeRegistry.getThingType(thingTypeUid);
245 assertNotNull(thingType);
247 List<ChannelDefinition> channelDefinitions = thingType.getChannelDefinitions();
248 assertNotNull(channelDefinitions);
250 List<Channel> channels = new ArrayList<Channel>();
251 for (ChannelDefinition channelDefinition : channelDefinitions) {
252 ChannelTypeUID channelTypeUid = channelDefinition.getChannelTypeUID();
253 assertNotNull(channelTypeUid);
255 ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUid);
256 assertNotNull(channelType);
258 String acceptedItemType = channelType.getItemType();
259 assertNotNull(acceptedItemType);
261 String channelId = channelDefinition.getId();
262 assertNotNull(channelId);
264 ChannelUID channelUid = new ChannelUID(thingUid, channelId);
265 assertNotNull(channelUid);
267 Channel channel = ChannelBuilder.create(channelUid, acceptedItemType).build();
268 assertNotNull(channel);
270 channels.add(channel);
276 private void createItemsForChannels(Thing thing) {
277 ItemBuilderFactory itemBuilderFactory = getService(ItemBuilderFactory.class);
278 assertNotNull(itemBuilderFactory);
280 for (Channel channel : thing.getChannels()) {
281 String acceptedItemType = channel.getAcceptedItemType();
282 assertNotNull(acceptedItemType);
284 ItemBuilder itemBuilder = itemBuilderFactory.newItemBuilder(Objects.requireNonNull(acceptedItemType),
285 channel.getUID().getId());
286 assertNotNull(itemBuilder);
288 Item item = itemBuilder.build();
291 getItemRegistry().add(item);
295 private void linkChannelsToItems(Thing thing) {
296 ItemChannelLinkRegistry itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class,
297 ItemChannelLinkRegistry.class);
298 assertNotNull(itemChannelLinkRegistry);
300 for (Channel channel : thing.getChannels()) {
301 String itemName = channel.getUID().getId();
302 assertNotNull(itemName);
304 ChannelUID channelUid = channel.getUID();
305 assertNotNull(channelUid);
307 ItemChannelLink link = itemChannelLinkRegistry.add(new ItemChannelLink(itemName, channelUid));
312 protected ChannelUID channel(String id) {
313 return new ChannelUID(getThingHandler().getThing().getUID(), id);
317 public void setUpAbstractMieleThingHandlerTest() throws Exception {
318 registerVolatileStorageService();
319 setUpThingRegistry();
323 thingHandler = setUpThingHandler();
326 private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
327 assertThingStatusIs(thing, expectedStatus, expectedStatusDetail, null);
330 private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
331 @Nullable String expectedDescription) {
332 assertEquals(expectedStatus, thing.getStatus());
333 assertEquals(expectedStatusDetail, thing.getStatusInfo().getStatusDetail());
334 if (expectedDescription == null) {
335 assertNull(thing.getStatusInfo().getDescription());
337 assertEquals(expectedDescription, thing.getStatusInfo().getDescription());
341 protected State getChannelState(String channelUid) {
342 Item item = getItemRegistry().get(channelUid);
343 assertNotNull(item, "Item for channel UID " + channelUid + " is null.");
344 return item.getState();
348 * Sets up the {@link ThingHandler} under test.
350 * @return The created {@link ThingHandler}.
352 protected abstract AbstractMieleThingHandler setUpThingHandler();
355 public void testCachedStateIsQueriedOnInitialize() throws Exception {
357 verify(getWebserviceMock()).dispatchDeviceState(SERIAL_NUMBER);
361 public void testThingStatusIsOfflineWithDetailGoneAndDetailMessageWhenDeviceIsRemoved() {
363 getBridgeHandler().onDeviceRemoved(SERIAL_NUMBER);
366 Thing thing = getThingHandler().getThing();
367 assertThingStatusIs(thing, ThingStatus.OFFLINE, ThingStatusDetail.GONE,
368 "@text/mielecloud.thing.status.removed");
371 private DeviceState createDeviceStateMock(StateType stateType, String localizedState) {
372 DeviceState deviceState = mock(DeviceState.class);
373 when(deviceState.getDeviceIdentifier()).thenReturn(getThingHandler().getThing().getUID().getId());
374 when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
375 when(deviceState.getStateType()).thenReturn(Optional.of(stateType));
376 when(deviceState.isInState(any())).thenCallRealMethod();
377 when(deviceState.getStatus()).thenReturn(Optional.of(localizedState));
382 public void testStatusIsSetToOnlineWhenDeviceStateIsValid() {
384 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
387 getBridgeHandler().onDeviceStateUpdated(deviceState);
390 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
394 public void testStatusIsSetToOfflineWhenDeviceIsNotConnected() {
396 DeviceState deviceState = createDeviceStateMock(StateType.NOT_CONNECTED, "Not connected");
399 getBridgeHandler().onDeviceStateUpdated(deviceState);
402 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
403 "@text/mielecloud.thing.status.disconnected");
407 public void testFailingPutProcessActionDoesNotSetTheDeviceToOffline() {
409 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
410 getBridgeHandler().onDeviceStateUpdated(deviceState);
411 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
413 doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putProcessAction(any(),
414 eq(ProcessAction.STOP));
417 getThingHandler().triggerProcessAction(ProcessAction.STOP);
420 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
424 public void testHandleCommandProgramStartToStartStopChannel() {
426 getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
427 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
430 waitForAssert(() -> {
431 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
436 public void testHandleCommandProgramStopToStartStopChannel() {
438 getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
439 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
442 waitForAssert(() -> {
443 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
448 public void testHandleCommandProgramStartToStartStopPauseChannel() {
450 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
451 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
454 waitForAssert(() -> {
455 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
460 public void testHandleCommandProgramStopToStartStopPauseChannel() {
462 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
463 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
466 waitForAssert(() -> {
467 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
472 public void testHandleCommandProgramPauseToStartStopPauseChannel() {
474 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
475 new StringType(ProgramStatus.PROGRAM_PAUSED.getState()));
478 waitForAssert(() -> {
479 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.PAUSE);
484 public void testFailingPutLightDoesNotSetTheDeviceToOffline() {
486 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
487 getBridgeHandler().onDeviceStateUpdated(deviceState);
488 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
490 doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putLight(any(), eq(true));
493 getThingHandler().triggerLight(true);
496 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
500 public void testHandleCommandLightOff() {
502 getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.OFF);
505 waitForAssert(() -> {
506 verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), false);
511 public void testHandleCommandLightOn() {
513 getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.ON);
516 waitForAssert(() -> {
517 verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), true);
522 public void testHandleCommandDoesNothingWhenCommandIsNotOfOnOffType() {
524 getThingHandler().handleCommand(channel(LIGHT_SWITCH), new DecimalType(0));
527 verify(getWebserviceMock(), never()).putLight(anyString(), anyBoolean());
531 public void testHandleCommandPowerOn() {
533 getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.ON);
536 waitForAssert(() -> {
537 verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), true);
542 public void testHandleCommandPowerOff() {
544 getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.OFF);
547 waitForAssert(() -> {
548 verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), false);
553 public void testHandleCommandDoesNothingWhenPowerCommandIsNotOfOnOffType() {
555 getThingHandler().handleCommand(channel(POWER_ON_OFF), new DecimalType(0));
558 verify(getWebserviceMock(), never()).putPowerState(anyString(), anyBoolean());
562 public void testMissingPropertiesAreSetWhenAStateUpdateIsReceivedFromTheCloud() {
564 assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_SERIAL_NUMBER));
565 assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_MODEL_ID));
567 var deviceState = mock(DeviceState.class);
568 when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
569 when(deviceState.getDeviceIdentifier()).thenReturn(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
570 when(deviceState.getFabNumber())
571 .thenReturn(Optional.of(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER));
572 when(deviceState.getType()).thenReturn(Optional.of("Unknown device type"));
573 when(deviceState.getTechType()).thenReturn(Optional.of("UK-4567"));
576 getThingHandler().onDeviceStateUpdated(deviceState);
579 assertEquals(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER,
580 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_SERIAL_NUMBER));
581 assertEquals("Unknown device type UK-4567",
582 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_MODEL_ID));