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);
132 private void setUpThingRegistry() {
133 thingRegistry = getService(ThingRegistry.class, ThingRegistry.class);
134 assertNotNull(thingRegistry, "Thing registry is missing");
137 private void setUpItemRegistry() {
138 itemRegistry = getService(ItemRegistry.class, ItemRegistry.class);
139 assertNotNull(itemRegistry);
142 private void setUpWebservice()
143 throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
144 webserviceMock = mock(MieleWebservice.class);
145 when(getWebserviceMock().hasAccessToken()).thenReturn(true);
147 MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
148 when(webserviceFactory.create(any())).thenReturn(getWebserviceMock());
150 MieleHandlerFactory factory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
151 assertNotNull(factory);
152 setPrivate(Objects.requireNonNull(factory), "webserviceFactory", webserviceFactory);
155 private void setUpBridge() throws Exception {
156 AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
157 accessTokenResponse.setAccessToken(ACCESS_TOKEN);
159 OAuthClientService oAuthClientService = mock(OAuthClientService.class);
160 when(oAuthClientService.getAccessTokenResponse()).thenReturn(accessTokenResponse);
162 OAuthFactory oAuthFactory = mock(OAuthFactory.class);
164 .getOAuthClientService(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString()))
165 .thenReturn(oAuthClientService);
167 OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
168 OpenHabOAuthTokenRefresher.class);
169 assertNotNull(tokenRefresher);
170 setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
172 bridge = BridgeBuilder
173 .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
174 MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
175 .withLabel("Miele@home Account")
177 new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
178 MieleCloudBindingIntegrationTestConstants.EMAIL)))
180 assertNotNull(bridge);
182 getThingRegistry().add(getBridge());
185 waitForAssert(() -> {
186 assertNotNull(getBridge().getHandler());
187 assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
190 MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) getBridge().getHandler();
191 assertNotNull(bridgeHandler);
193 waitForAssert(() -> {
194 assertNotNull(bridgeHandler.getThing());
197 bridgeHandler.initialize();
198 bridgeHandler.onConnectionAlive();
199 setPrivate(bridgeHandler, "discoveryService", null);
200 this.bridgeHandler = bridgeHandler;
203 protected AbstractMieleThingHandler createThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid,
204 Class<? extends AbstractMieleThingHandler> expectedHandlerClass, String deviceIdentifier) {
205 ThingRegistry registry = getThingRegistry();
207 List<Channel> channels = createChannelsForThingHandler(thingTypeUid, thingUid);
209 Thing thing = ThingBuilder.create(thingTypeUid, thingUid)
210 .withConfiguration(new Configuration(Collections
211 .singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, deviceIdentifier)))
212 .withBridge(getBridge().getUID()).withChannels(channels).withLabel("DA-6996").build();
213 assertNotNull(thing);
217 waitForAssert(() -> {
218 ThingHandler handler = thing.getHandler();
219 assertNotNull(handler);
220 assertTrue(expectedHandlerClass.isAssignableFrom(handler.getClass()), "Handler type is wrong");
223 createItemsForChannels(thing);
224 linkChannelsToItems(thing);
226 ThingHandler handler = thing.getHandler();
227 assertNotNull(handler);
228 return (AbstractMieleThingHandler) Objects.requireNonNull(handler);
231 private List<Channel> createChannelsForThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid) {
232 ChannelTypeRegistry channelTypeRegistry = getService(ChannelTypeRegistry.class, ChannelTypeRegistry.class);
233 assertNotNull(channelTypeRegistry);
235 ThingTypeRegistry thingTypeRegistry = getService(ThingTypeRegistry.class, ThingTypeRegistry.class);
236 assertNotNull(thingTypeRegistry);
238 ThingType thingType = thingTypeRegistry.getThingType(thingTypeUid);
239 assertNotNull(thingType);
241 List<ChannelDefinition> channelDefinitions = thingType.getChannelDefinitions();
242 assertNotNull(channelDefinitions);
244 List<Channel> channels = new ArrayList<Channel>();
245 for (ChannelDefinition channelDefinition : channelDefinitions) {
246 ChannelTypeUID channelTypeUid = channelDefinition.getChannelTypeUID();
247 assertNotNull(channelTypeUid);
249 ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUid);
250 assertNotNull(channelType);
252 String acceptedItemType = channelType.getItemType();
253 assertNotNull(acceptedItemType);
255 String channelId = channelDefinition.getId();
256 assertNotNull(channelId);
258 ChannelUID channelUid = new ChannelUID(thingUid, channelId);
259 assertNotNull(channelUid);
261 Channel channel = ChannelBuilder.create(channelUid, acceptedItemType).build();
262 assertNotNull(channel);
264 channels.add(channel);
270 private void createItemsForChannels(Thing thing) {
271 ItemBuilderFactory itemBuilderFactory = getService(ItemBuilderFactory.class);
272 assertNotNull(itemBuilderFactory);
274 for (Channel channel : thing.getChannels()) {
275 String acceptedItemType = channel.getAcceptedItemType();
276 assertNotNull(acceptedItemType);
278 ItemBuilder itemBuilder = itemBuilderFactory.newItemBuilder(Objects.requireNonNull(acceptedItemType),
279 channel.getUID().getId());
280 assertNotNull(itemBuilder);
282 Item item = itemBuilder.build();
285 getItemRegistry().add(item);
289 private void linkChannelsToItems(Thing thing) {
290 ItemChannelLinkRegistry itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class,
291 ItemChannelLinkRegistry.class);
292 assertNotNull(itemChannelLinkRegistry);
294 for (Channel channel : thing.getChannels()) {
295 String itemName = channel.getUID().getId();
296 assertNotNull(itemName);
298 ChannelUID channelUid = channel.getUID();
299 assertNotNull(channelUid);
301 ItemChannelLink link = itemChannelLinkRegistry.add(new ItemChannelLink(itemName, channelUid));
306 protected ChannelUID channel(String id) {
307 return new ChannelUID(getThingHandler().getThing().getUID(), id);
311 public void setUpAbstractMieleThingHandlerTest() throws Exception {
312 registerVolatileStorageService();
313 setUpThingRegistry();
317 thingHandler = setUpThingHandler();
320 private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
321 assertThingStatusIs(thing, expectedStatus, expectedStatusDetail, null);
324 private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
325 @Nullable String expectedDescription) {
326 assertEquals(expectedStatus, thing.getStatus());
327 assertEquals(expectedStatusDetail, thing.getStatusInfo().getStatusDetail());
328 if (expectedDescription == null) {
329 assertNull(thing.getStatusInfo().getDescription());
331 assertEquals(expectedDescription, thing.getStatusInfo().getDescription());
335 protected State getChannelState(String channelUid) {
336 Item item = getItemRegistry().get(channelUid);
337 assertNotNull(item, "Item for channel UID " + channelUid + " is null.");
338 return item.getState();
342 * Sets up the {@link ThingHandler} under test.
344 * @return The created {@link ThingHandler}.
346 protected abstract AbstractMieleThingHandler setUpThingHandler();
349 public void testCachedStateIsQueriedOnInitialize() throws Exception {
351 verify(getWebserviceMock()).dispatchDeviceState(SERIAL_NUMBER);
355 public void testThingStatusIsOfflineWithDetailGoneAndDetailMessageWhenDeviceIsRemoved() {
357 getBridgeHandler().onDeviceRemoved(SERIAL_NUMBER);
360 Thing thing = getThingHandler().getThing();
361 assertThingStatusIs(thing, ThingStatus.OFFLINE, ThingStatusDetail.GONE,
362 "@text/mielecloud.thing.status.removed");
365 private DeviceState createDeviceStateMock(StateType stateType, String localizedState) {
366 DeviceState deviceState = mock(DeviceState.class);
367 when(deviceState.getDeviceIdentifier()).thenReturn(getThingHandler().getThing().getUID().getId());
368 when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
369 when(deviceState.getStateType()).thenReturn(Optional.of(stateType));
370 when(deviceState.isInState(any())).thenCallRealMethod();
371 when(deviceState.getStatus()).thenReturn(Optional.of(localizedState));
376 public void testStatusIsSetToOnlineWhenDeviceStateIsValid() {
378 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
381 getBridgeHandler().onDeviceStateUpdated(deviceState);
384 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
388 public void testStatusIsSetToOfflineWhenDeviceIsNotConnected() {
390 DeviceState deviceState = createDeviceStateMock(StateType.NOT_CONNECTED, "Not connected");
393 getBridgeHandler().onDeviceStateUpdated(deviceState);
396 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
397 "@text/mielecloud.thing.status.disconnected");
401 public void testFailingPutProcessActionDoesNotSetTheDeviceToOffline() {
403 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
404 getBridgeHandler().onDeviceStateUpdated(deviceState);
405 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
407 doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putProcessAction(any(),
408 eq(ProcessAction.STOP));
411 getThingHandler().triggerProcessAction(ProcessAction.STOP);
414 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
418 public void testHandleCommandProgramStartToStartStopChannel() {
420 getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
421 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
424 waitForAssert(() -> {
425 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
430 public void testHandleCommandProgramStopToStartStopChannel() {
432 getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
433 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
436 waitForAssert(() -> {
437 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
442 public void testHandleCommandProgramStartToStartStopPauseChannel() {
444 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
445 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
448 waitForAssert(() -> {
449 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
454 public void testHandleCommandProgramStopToStartStopPauseChannel() {
456 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
457 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
460 waitForAssert(() -> {
461 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
466 public void testHandleCommandProgramPauseToStartStopPauseChannel() {
468 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
469 new StringType(ProgramStatus.PROGRAM_PAUSED.getState()));
472 waitForAssert(() -> {
473 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.PAUSE);
478 public void testFailingPutLightDoesNotSetTheDeviceToOffline() {
480 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
481 getBridgeHandler().onDeviceStateUpdated(deviceState);
482 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
484 doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putLight(any(), eq(true));
487 getThingHandler().triggerLight(true);
490 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
494 public void testHandleCommandLightOff() {
496 getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.OFF);
499 waitForAssert(() -> {
500 verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), false);
505 public void testHandleCommandLightOn() {
507 getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.ON);
510 waitForAssert(() -> {
511 verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), true);
516 public void testHandleCommandDoesNothingWhenCommandIsNotOfOnOffType() {
518 getThingHandler().handleCommand(channel(LIGHT_SWITCH), new DecimalType(0));
521 verify(getWebserviceMock(), never()).putLight(anyString(), anyBoolean());
525 public void testHandleCommandPowerOn() {
527 getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.ON);
530 waitForAssert(() -> {
531 verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), true);
536 public void testHandleCommandPowerOff() {
538 getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.OFF);
541 waitForAssert(() -> {
542 verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), false);
547 public void testHandleCommandDoesNothingWhenPowerCommandIsNotOfOnOffType() {
549 getThingHandler().handleCommand(channel(POWER_ON_OFF), new DecimalType(0));
552 verify(getWebserviceMock(), never()).putPowerState(anyString(), anyBoolean());
556 public void testMissingPropertiesAreSetWhenAStateUpdateIsReceivedFromTheCloud() {
558 assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_SERIAL_NUMBER));
559 assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_MODEL_ID));
561 var deviceState = mock(DeviceState.class);
562 when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
563 when(deviceState.getDeviceIdentifier()).thenReturn(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
564 when(deviceState.getFabNumber())
565 .thenReturn(Optional.of(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER));
566 when(deviceState.getType()).thenReturn(Optional.of("Unknown device type"));
567 when(deviceState.getTechType()).thenReturn(Optional.of("UK-4567"));
570 getThingHandler().onDeviceStateUpdated(deviceState);
573 assertEquals(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER,
574 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_SERIAL_NUMBER));
575 assertEquals("Unknown device type UK-4567",
576 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_MODEL_ID));