2 * Copyright (c) 2010-2022 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.AfterEach;
31 import org.junit.jupiter.api.BeforeEach;
32 import org.junit.jupiter.api.Test;
33 import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
34 import org.openhab.binding.mielecloud.internal.auth.OAuthTokenRefresher;
35 import org.openhab.binding.mielecloud.internal.auth.OpenHabOAuthTokenRefresher;
36 import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
37 import org.openhab.binding.mielecloud.internal.util.ReflectionUtil;
38 import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
39 import org.openhab.binding.mielecloud.internal.webservice.MieleWebserviceFactory;
40 import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
41 import org.openhab.binding.mielecloud.internal.webservice.api.ProgramStatus;
42 import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
43 import org.openhab.binding.mielecloud.internal.webservice.api.json.ProcessAction;
44 import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
45 import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
46 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
47 import org.openhab.core.auth.client.oauth2.OAuthClientService;
48 import org.openhab.core.auth.client.oauth2.OAuthFactory;
49 import org.openhab.core.config.core.Configuration;
50 import org.openhab.core.items.Item;
51 import org.openhab.core.items.ItemBuilder;
52 import org.openhab.core.items.ItemBuilderFactory;
53 import org.openhab.core.items.ItemRegistry;
54 import org.openhab.core.library.types.DecimalType;
55 import org.openhab.core.library.types.OnOffType;
56 import org.openhab.core.library.types.StringType;
57 import org.openhab.core.test.java.JavaOSGiTest;
58 import org.openhab.core.thing.Bridge;
59 import org.openhab.core.thing.Channel;
60 import org.openhab.core.thing.ChannelUID;
61 import org.openhab.core.thing.Thing;
62 import org.openhab.core.thing.ThingRegistry;
63 import org.openhab.core.thing.ThingStatus;
64 import org.openhab.core.thing.ThingStatusDetail;
65 import org.openhab.core.thing.ThingTypeUID;
66 import org.openhab.core.thing.ThingUID;
67 import org.openhab.core.thing.binding.ThingHandler;
68 import org.openhab.core.thing.binding.ThingHandlerFactory;
69 import org.openhab.core.thing.binding.builder.BridgeBuilder;
70 import org.openhab.core.thing.binding.builder.ChannelBuilder;
71 import org.openhab.core.thing.binding.builder.ThingBuilder;
72 import org.openhab.core.thing.link.ItemChannelLink;
73 import org.openhab.core.thing.link.ItemChannelLinkRegistry;
74 import org.openhab.core.thing.type.ChannelDefinition;
75 import org.openhab.core.thing.type.ChannelType;
76 import org.openhab.core.thing.type.ChannelTypeRegistry;
77 import org.openhab.core.thing.type.ChannelTypeUID;
78 import org.openhab.core.thing.type.ThingType;
79 import org.openhab.core.thing.type.ThingTypeRegistry;
80 import org.openhab.core.types.State;
81 import org.openhab.core.types.UnDefType;
84 * @author Björn Lange - Initial contribution
87 public abstract class AbstractMieleThingHandlerTest extends JavaOSGiTest {
88 protected static final State NULL_VALUE_STATE = UnDefType.UNDEF;
91 private Bridge bridge;
93 private MieleBridgeHandler bridgeHandler;
95 private ThingRegistry thingRegistry;
97 private MieleWebservice webserviceMock;
99 private AbstractMieleThingHandler thingHandler;
102 private ItemRegistry itemRegistry;
104 protected Bridge getBridge() {
105 assertNotNull(bridge);
106 return Objects.requireNonNull(bridge);
109 protected MieleBridgeHandler getBridgeHandler() {
110 assertNotNull(bridgeHandler);
111 return Objects.requireNonNull(bridgeHandler);
114 protected ThingRegistry getThingRegistry() {
115 assertNotNull(thingRegistry);
116 return Objects.requireNonNull(thingRegistry);
119 protected MieleWebservice getWebserviceMock() {
120 assertNotNull(webserviceMock);
121 return Objects.requireNonNull(webserviceMock);
124 protected AbstractMieleThingHandler getThingHandler() {
125 assertNotNull(thingHandler);
126 return Objects.requireNonNull(thingHandler);
129 protected ItemRegistry getItemRegistry() {
130 assertNotNull(itemRegistry);
131 return Objects.requireNonNull(itemRegistry);
135 protected void waitForAssert(Runnable assertion) {
136 // Use a larger timeout to avoid test failures in the CI build.
137 waitForAssert(assertion, 2 * DFL_TIMEOUT, 2 * DFL_SLEEP_TIME);
140 private void setUpThingRegistry() {
141 thingRegistry = getService(ThingRegistry.class, ThingRegistry.class);
142 assertNotNull(thingRegistry, "Thing registry is missing");
145 private void setUpItemRegistry() {
146 itemRegistry = getService(ItemRegistry.class, ItemRegistry.class);
147 assertNotNull(itemRegistry);
150 private void setUpWebservice()
151 throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
152 webserviceMock = mock(MieleWebservice.class);
153 when(getWebserviceMock().hasAccessToken()).thenReturn(true);
155 MieleWebserviceFactory webserviceFactory = mock(MieleWebserviceFactory.class);
156 when(webserviceFactory.create(any())).thenReturn(getWebserviceMock());
158 MieleHandlerFactory factory = getService(ThingHandlerFactory.class, MieleHandlerFactory.class);
159 assertNotNull(factory);
160 setPrivate(Objects.requireNonNull(factory), "webserviceFactory", webserviceFactory);
163 private void setUpBridge() throws Exception {
164 AccessTokenResponse accessTokenResponse = new AccessTokenResponse();
165 accessTokenResponse.setAccessToken(ACCESS_TOKEN);
167 OAuthClientService oAuthClientService = mock(OAuthClientService.class);
168 when(oAuthClientService.getAccessTokenResponse()).thenReturn(accessTokenResponse);
170 OAuthFactory oAuthFactory = mock(OAuthFactory.class);
172 .getOAuthClientService(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID.getAsString()))
173 .thenReturn(oAuthClientService);
175 OpenHabOAuthTokenRefresher tokenRefresher = getService(OAuthTokenRefresher.class,
176 OpenHabOAuthTokenRefresher.class);
177 assertNotNull(tokenRefresher);
178 setPrivate(Objects.requireNonNull(tokenRefresher), "oauthFactory", oAuthFactory);
180 bridge = BridgeBuilder
181 .create(MieleCloudBindingConstants.THING_TYPE_BRIDGE,
182 MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID)
183 .withLabel("Miele@home Account")
185 new Configuration(Collections.singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL,
186 MieleCloudBindingIntegrationTestConstants.EMAIL)))
188 assertNotNull(bridge);
190 getThingRegistry().add(getBridge());
193 waitForAssert(() -> {
194 assertNotNull(getBridge().getHandler());
195 assertTrue(getBridge().getHandler() instanceof MieleBridgeHandler, "Handler type is wrong");
198 MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) getBridge().getHandler();
199 assertNotNull(bridgeHandler);
201 waitForAssert(() -> {
202 assertNotNull(bridgeHandler.getThing());
205 bridgeHandler.initialize();
206 bridgeHandler.onConnectionAlive();
207 setPrivate(bridgeHandler, "discoveryService", null);
208 this.bridgeHandler = bridgeHandler;
211 protected AbstractMieleThingHandler createThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid,
212 Class<? extends AbstractMieleThingHandler> expectedHandlerClass, String deviceIdentifier) {
213 ThingRegistry registry = getThingRegistry();
215 List<Channel> channels = createChannelsForThingHandler(thingTypeUid, thingUid);
217 Thing thing = ThingBuilder.create(thingTypeUid, thingUid)
218 .withConfiguration(new Configuration(Collections
219 .singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, deviceIdentifier)))
220 .withBridge(getBridge().getUID()).withChannels(channels).withLabel("DA-6996").build();
221 assertNotNull(thing);
225 waitForAssert(() -> {
226 ThingHandler handler = thing.getHandler();
227 assertNotNull(handler);
228 assertTrue(expectedHandlerClass.isAssignableFrom(handler.getClass()), "Handler type is wrong");
231 createItemsForChannels(thing);
232 linkChannelsToItems(thing);
234 ThingHandler handler = thing.getHandler();
235 assertNotNull(handler);
236 AbstractMieleThingHandler mieleThingHandler = (AbstractMieleThingHandler) Objects.requireNonNull(handler);
238 waitForAssert(() -> {
240 assertNotNull(ReflectionUtil.invokePrivate(mieleThingHandler, "getBridge"));
241 } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) {
242 throw new RuntimeException(e);
244 assertNotNull(getBridge().getThing(thingUid));
247 return mieleThingHandler;
250 private List<Channel> createChannelsForThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid) {
251 ChannelTypeRegistry channelTypeRegistry = getService(ChannelTypeRegistry.class, ChannelTypeRegistry.class);
252 assertNotNull(channelTypeRegistry);
254 ThingTypeRegistry thingTypeRegistry = getService(ThingTypeRegistry.class, ThingTypeRegistry.class);
255 assertNotNull(thingTypeRegistry);
257 ThingType thingType = thingTypeRegistry.getThingType(thingTypeUid);
258 assertNotNull(thingType);
260 List<ChannelDefinition> channelDefinitions = thingType.getChannelDefinitions();
261 assertNotNull(channelDefinitions);
263 List<Channel> channels = new ArrayList<Channel>();
264 for (ChannelDefinition channelDefinition : channelDefinitions) {
265 ChannelTypeUID channelTypeUid = channelDefinition.getChannelTypeUID();
266 assertNotNull(channelTypeUid);
268 ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUid);
269 assertNotNull(channelType);
271 String acceptedItemType = channelType.getItemType();
272 assertNotNull(acceptedItemType);
274 String channelId = channelDefinition.getId();
275 assertNotNull(channelId);
277 ChannelUID channelUid = new ChannelUID(thingUid, channelId);
278 assertNotNull(channelUid);
280 Channel channel = ChannelBuilder.create(channelUid, acceptedItemType).build();
281 assertNotNull(channel);
283 channels.add(channel);
289 private void createItemsForChannels(Thing thing) {
290 ItemBuilderFactory itemBuilderFactory = getService(ItemBuilderFactory.class);
291 assertNotNull(itemBuilderFactory);
293 for (Channel channel : thing.getChannels()) {
294 String acceptedItemType = channel.getAcceptedItemType();
295 assertNotNull(acceptedItemType);
297 ItemBuilder itemBuilder = itemBuilderFactory.newItemBuilder(Objects.requireNonNull(acceptedItemType),
298 channel.getUID().getId());
299 assertNotNull(itemBuilder);
301 Item item = itemBuilder.build();
304 getItemRegistry().add(item);
308 private void linkChannelsToItems(Thing thing) {
309 ItemChannelLinkRegistry itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class,
310 ItemChannelLinkRegistry.class);
311 assertNotNull(itemChannelLinkRegistry);
313 for (Channel channel : thing.getChannels()) {
314 String itemName = channel.getUID().getId();
315 assertNotNull(itemName);
317 ChannelUID channelUid = channel.getUID();
318 assertNotNull(channelUid);
320 ItemChannelLink link = itemChannelLinkRegistry.add(new ItemChannelLink(itemName, channelUid));
325 protected ChannelUID channel(String id) {
326 return new ChannelUID(getThingHandler().getThing().getUID(), id);
330 public void setUpAbstractMieleThingHandlerTest() throws Exception {
331 registerVolatileStorageService();
332 setUpThingRegistry();
337 protected void setUpBridgeAndThing() throws Exception {
339 thingHandler = setUpThingHandler();
342 private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
343 assertThingStatusIs(thing, expectedStatus, expectedStatusDetail, null);
346 private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
347 @Nullable String expectedDescription) {
348 assertEquals(expectedStatus, thing.getStatus());
349 assertEquals(expectedStatusDetail, thing.getStatusInfo().getStatusDetail());
350 if (expectedDescription == null) {
351 assertNull(thing.getStatusInfo().getDescription());
353 assertEquals(expectedDescription, thing.getStatusInfo().getDescription());
357 protected State getChannelState(String channelUid) {
358 Item item = getItemRegistry().get(channelUid);
359 assertNotNull(item, "Item for channel UID " + channelUid + " is null.");
360 return item.getState();
364 * Sets up the {@link ThingHandler} under test.
366 * @return The created {@link ThingHandler}.
368 protected abstract AbstractMieleThingHandler setUpThingHandler();
371 public void tearDownAbstractMieleThingHandlerTest() {
372 getThingRegistry().forceRemove(getThingHandler().getThing().getUID());
373 getThingRegistry().forceRemove(getBridge().getUID());
377 public void testCachedStateIsQueriedOnInitialize() throws Exception {
379 setUpBridgeAndThing();
382 verify(getWebserviceMock()).dispatchDeviceState(SERIAL_NUMBER);
386 public void testThingStatusIsOfflineWithDetailGoneAndDetailMessageWhenDeviceIsRemoved() throws Exception {
388 setUpBridgeAndThing();
391 getBridgeHandler().onDeviceRemoved(SERIAL_NUMBER);
394 Thing thing = getThingHandler().getThing();
395 assertThingStatusIs(thing, ThingStatus.OFFLINE, ThingStatusDetail.GONE,
396 "@text/mielecloud.thing.status.removed");
399 private DeviceState createDeviceStateMock(StateType stateType, String localizedState) {
400 DeviceState deviceState = mock(DeviceState.class);
401 when(deviceState.getDeviceIdentifier()).thenReturn(getThingHandler().getThing().getUID().getId());
402 when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
403 when(deviceState.getStateType()).thenReturn(Optional.of(stateType));
404 when(deviceState.isInState(any())).thenCallRealMethod();
405 when(deviceState.getStatus()).thenReturn(Optional.of(localizedState));
410 public void testStatusIsSetToOnlineWhenDeviceStateIsValid() throws Exception {
412 setUpBridgeAndThing();
414 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
417 getBridgeHandler().onDeviceStateUpdated(deviceState);
420 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
424 public void testStatusIsSetToOfflineWhenDeviceIsNotConnected() throws Exception {
426 setUpBridgeAndThing();
428 DeviceState deviceState = createDeviceStateMock(StateType.NOT_CONNECTED, "Not connected");
431 getBridgeHandler().onDeviceStateUpdated(deviceState);
434 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
435 "@text/mielecloud.thing.status.disconnected");
439 public void testFailingPutProcessActionDoesNotSetTheDeviceToOffline() throws Exception {
441 doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putProcessAction(any(),
442 eq(ProcessAction.STOP));
444 setUpBridgeAndThing();
446 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
447 getBridgeHandler().onDeviceStateUpdated(deviceState);
448 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
451 getThingHandler().triggerProcessAction(ProcessAction.STOP);
454 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
458 public void testHandleCommandProgramStartToStartStopChannel() throws Exception {
460 setUpBridgeAndThing();
463 getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
464 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
467 waitForAssert(() -> {
468 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
473 public void testHandleCommandProgramStopToStartStopChannel() throws Exception {
475 setUpBridgeAndThing();
478 getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
479 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
482 waitForAssert(() -> {
483 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
488 public void testHandleCommandProgramStartToStartStopPauseChannel() throws Exception {
490 setUpBridgeAndThing();
493 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
494 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
497 waitForAssert(() -> {
498 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
503 public void testHandleCommandProgramStopToStartStopPauseChannel() throws Exception {
505 setUpBridgeAndThing();
508 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
509 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
512 waitForAssert(() -> {
513 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
518 public void testHandleCommandProgramPauseToStartStopPauseChannel() throws Exception {
520 setUpBridgeAndThing();
523 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
524 new StringType(ProgramStatus.PROGRAM_PAUSED.getState()));
527 waitForAssert(() -> {
528 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.PAUSE);
533 public void testFailingPutLightDoesNotSetTheDeviceToOffline() throws Exception {
535 doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putLight(any(), eq(true));
537 setUpBridgeAndThing();
539 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
540 getBridgeHandler().onDeviceStateUpdated(deviceState);
541 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
544 getThingHandler().triggerLight(true);
547 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
551 public void testHandleCommandLightOff() throws Exception {
553 setUpBridgeAndThing();
556 getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.OFF);
559 waitForAssert(() -> {
560 verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), false);
565 public void testHandleCommandLightOn() throws Exception {
567 setUpBridgeAndThing();
570 getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.ON);
573 waitForAssert(() -> {
574 verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), true);
579 public void testHandleCommandDoesNothingWhenCommandIsNotOfOnOffType() throws Exception {
581 setUpBridgeAndThing();
584 getThingHandler().handleCommand(channel(LIGHT_SWITCH), new DecimalType(0));
587 verify(getWebserviceMock(), never()).putLight(anyString(), anyBoolean());
591 public void testHandleCommandPowerOn() throws Exception {
593 setUpBridgeAndThing();
596 getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.ON);
599 waitForAssert(() -> {
600 verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), true);
605 public void testHandleCommandPowerOff() throws Exception {
607 setUpBridgeAndThing();
610 getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.OFF);
613 waitForAssert(() -> {
614 verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), false);
619 public void testHandleCommandDoesNothingWhenPowerCommandIsNotOfOnOffType() throws Exception {
621 setUpBridgeAndThing();
624 getThingHandler().handleCommand(channel(POWER_ON_OFF), new DecimalType(0));
627 verify(getWebserviceMock(), never()).putPowerState(anyString(), anyBoolean());
631 public void testMissingPropertiesAreSetWhenAStateUpdateIsReceivedFromTheCloud() throws Exception {
633 setUpBridgeAndThing();
635 assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_SERIAL_NUMBER));
636 assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_MODEL_ID));
638 var deviceState = mock(DeviceState.class);
639 when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
640 when(deviceState.getDeviceIdentifier()).thenReturn(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
641 when(deviceState.getFabNumber())
642 .thenReturn(Optional.of(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER));
643 when(deviceState.getType()).thenReturn(Optional.of("Unknown device type"));
644 when(deviceState.getTechType()).thenReturn(Optional.of("UK-4567"));
647 getThingHandler().onDeviceStateUpdated(deviceState);
650 assertEquals(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER,
651 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_SERIAL_NUMBER));
652 assertEquals("Unknown device type UK-4567",
653 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_MODEL_ID));