2 * Copyright (c) 2010-2023 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 String thingTypeVersion) {
214 ThingRegistry registry = getThingRegistry();
216 List<Channel> channels = createChannelsForThingHandler(thingTypeUid, thingUid);
218 Thing thing = ThingBuilder.create(thingTypeUid, thingUid)
219 .withConfiguration(new Configuration(Collections
220 .singletonMap(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, deviceIdentifier)))
221 .withBridge(getBridge().getUID()).withChannels(channels).withLabel("DA-6996")
222 .withProperty("thingTypeVersion", thingTypeVersion).build();
223 assertNotNull(thing);
227 waitForAssert(() -> {
228 ThingHandler handler = thing.getHandler();
229 assertNotNull(handler);
230 assertTrue(expectedHandlerClass.isAssignableFrom(handler.getClass()), "Handler type is wrong");
233 createItemsForChannels(thing);
234 linkChannelsToItems(thing);
236 ThingHandler handler = thing.getHandler();
237 assertNotNull(handler);
238 AbstractMieleThingHandler mieleThingHandler = (AbstractMieleThingHandler) Objects.requireNonNull(handler);
240 waitForAssert(() -> {
242 assertNotNull(ReflectionUtil.invokePrivate(mieleThingHandler, "getBridge"));
243 } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) {
244 throw new RuntimeException(e);
246 assertNotNull(getBridge().getThing(thingUid));
249 return mieleThingHandler;
252 private List<Channel> createChannelsForThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid) {
253 ChannelTypeRegistry channelTypeRegistry = getService(ChannelTypeRegistry.class, ChannelTypeRegistry.class);
254 assertNotNull(channelTypeRegistry);
256 ThingTypeRegistry thingTypeRegistry = getService(ThingTypeRegistry.class, ThingTypeRegistry.class);
257 assertNotNull(thingTypeRegistry);
259 ThingType thingType = thingTypeRegistry.getThingType(thingTypeUid);
260 assertNotNull(thingType);
262 List<ChannelDefinition> channelDefinitions = thingType.getChannelDefinitions();
263 assertNotNull(channelDefinitions);
265 List<Channel> channels = new ArrayList<Channel>();
266 for (ChannelDefinition channelDefinition : channelDefinitions) {
267 ChannelTypeUID channelTypeUid = channelDefinition.getChannelTypeUID();
268 assertNotNull(channelTypeUid);
270 ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUid);
271 assertNotNull(channelType);
273 String acceptedItemType = channelType.getItemType();
274 assertNotNull(acceptedItemType);
276 String channelId = channelDefinition.getId();
277 assertNotNull(channelId);
279 ChannelUID channelUid = new ChannelUID(thingUid, channelId);
280 assertNotNull(channelUid);
282 Channel channel = ChannelBuilder.create(channelUid, acceptedItemType).build();
283 assertNotNull(channel);
285 channels.add(channel);
291 private void createItemsForChannels(Thing thing) {
292 ItemBuilderFactory itemBuilderFactory = getService(ItemBuilderFactory.class);
293 assertNotNull(itemBuilderFactory);
295 for (Channel channel : thing.getChannels()) {
296 String acceptedItemType = channel.getAcceptedItemType();
297 assertNotNull(acceptedItemType);
299 ItemBuilder itemBuilder = itemBuilderFactory.newItemBuilder(Objects.requireNonNull(acceptedItemType),
300 channel.getUID().getId());
301 assertNotNull(itemBuilder);
303 Item item = itemBuilder.build();
306 getItemRegistry().add(item);
310 private void linkChannelsToItems(Thing thing) {
311 ItemChannelLinkRegistry itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class,
312 ItemChannelLinkRegistry.class);
313 assertNotNull(itemChannelLinkRegistry);
315 for (Channel channel : thing.getChannels()) {
316 String itemName = channel.getUID().getId();
317 assertNotNull(itemName);
319 ChannelUID channelUid = channel.getUID();
320 assertNotNull(channelUid);
322 ItemChannelLink link = itemChannelLinkRegistry.add(new ItemChannelLink(itemName, channelUid));
327 protected ChannelUID channel(String id) {
328 return new ChannelUID(getThingHandler().getThing().getUID(), id);
332 public void setUpAbstractMieleThingHandlerTest() throws Exception {
333 registerVolatileStorageService();
334 setUpThingRegistry();
339 protected void setUpBridgeAndThing() throws Exception {
341 thingHandler = setUpThingHandler();
344 private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
345 assertThingStatusIs(thing, expectedStatus, expectedStatusDetail, null);
348 private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
349 @Nullable String expectedDescription) {
350 assertEquals(expectedStatus, thing.getStatus());
351 assertEquals(expectedStatusDetail, thing.getStatusInfo().getStatusDetail());
352 if (expectedDescription == null) {
353 assertNull(thing.getStatusInfo().getDescription());
355 assertEquals(expectedDescription, thing.getStatusInfo().getDescription());
359 protected State getChannelState(String channelUid) {
360 Item item = getItemRegistry().get(channelUid);
361 assertNotNull(item, "Item for channel UID " + channelUid + " is null.");
362 return item.getState();
366 * Sets up the {@link ThingHandler} under test.
368 * @return The created {@link ThingHandler}.
370 protected abstract AbstractMieleThingHandler setUpThingHandler();
373 public void tearDownAbstractMieleThingHandlerTest() {
374 getThingRegistry().forceRemove(getThingHandler().getThing().getUID());
375 getThingRegistry().forceRemove(getBridge().getUID());
379 public void testCachedStateIsQueriedOnInitialize() throws Exception {
381 setUpBridgeAndThing();
384 verify(getWebserviceMock()).dispatchDeviceState(SERIAL_NUMBER);
388 public void testThingStatusIsOfflineWithDetailGoneAndDetailMessageWhenDeviceIsRemoved() throws Exception {
390 setUpBridgeAndThing();
393 getBridgeHandler().onDeviceRemoved(SERIAL_NUMBER);
396 Thing thing = getThingHandler().getThing();
397 assertThingStatusIs(thing, ThingStatus.OFFLINE, ThingStatusDetail.GONE,
398 "@text/mielecloud.thing.status.removed");
401 private DeviceState createDeviceStateMock(StateType stateType, String localizedState) {
402 DeviceState deviceState = mock(DeviceState.class);
403 when(deviceState.getDeviceIdentifier()).thenReturn(getThingHandler().getThing().getUID().getId());
404 when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
405 when(deviceState.getStateType()).thenReturn(Optional.of(stateType));
406 when(deviceState.isInState(any())).thenCallRealMethod();
407 when(deviceState.getStatus()).thenReturn(Optional.of(localizedState));
412 public void testStatusIsSetToOnlineWhenDeviceStateIsValid() throws Exception {
414 setUpBridgeAndThing();
416 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
419 getBridgeHandler().onDeviceStateUpdated(deviceState);
422 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
426 public void testStatusIsSetToOfflineWhenDeviceIsNotConnected() throws Exception {
428 setUpBridgeAndThing();
430 DeviceState deviceState = createDeviceStateMock(StateType.NOT_CONNECTED, "Not connected");
433 getBridgeHandler().onDeviceStateUpdated(deviceState);
436 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
437 "@text/mielecloud.thing.status.disconnected");
441 public void testFailingPutProcessActionDoesNotSetTheDeviceToOffline() throws Exception {
443 doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putProcessAction(any(),
444 eq(ProcessAction.STOP));
446 setUpBridgeAndThing();
448 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
449 getBridgeHandler().onDeviceStateUpdated(deviceState);
450 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
453 getThingHandler().triggerProcessAction(ProcessAction.STOP);
456 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
460 public void testHandleCommandProgramStartToStartStopChannel() throws Exception {
462 setUpBridgeAndThing();
465 getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
466 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
469 waitForAssert(() -> {
470 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
475 public void testHandleCommandProgramStopToStartStopChannel() throws Exception {
477 setUpBridgeAndThing();
480 getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
481 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
484 waitForAssert(() -> {
485 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
490 public void testHandleCommandProgramStartToStartStopPauseChannel() throws Exception {
492 setUpBridgeAndThing();
495 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
496 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
499 waitForAssert(() -> {
500 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
505 public void testHandleCommandProgramStopToStartStopPauseChannel() throws Exception {
507 setUpBridgeAndThing();
510 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
511 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
514 waitForAssert(() -> {
515 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
520 public void testHandleCommandProgramPauseToStartStopPauseChannel() throws Exception {
522 setUpBridgeAndThing();
525 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
526 new StringType(ProgramStatus.PROGRAM_PAUSED.getState()));
529 waitForAssert(() -> {
530 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.PAUSE);
535 public void testFailingPutLightDoesNotSetTheDeviceToOffline() throws Exception {
537 doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putLight(any(), eq(true));
539 setUpBridgeAndThing();
541 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
542 getBridgeHandler().onDeviceStateUpdated(deviceState);
543 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
546 getThingHandler().triggerLight(true);
549 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
553 public void testHandleCommandLightOff() throws Exception {
555 setUpBridgeAndThing();
558 getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.OFF);
561 waitForAssert(() -> {
562 verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), false);
567 public void testHandleCommandLightOn() throws Exception {
569 setUpBridgeAndThing();
572 getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.ON);
575 waitForAssert(() -> {
576 verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), true);
581 public void testHandleCommandDoesNothingWhenCommandIsNotOfOnOffType() throws Exception {
583 setUpBridgeAndThing();
586 getThingHandler().handleCommand(channel(LIGHT_SWITCH), new DecimalType(0));
589 verify(getWebserviceMock(), never()).putLight(anyString(), anyBoolean());
593 public void testHandleCommandPowerOn() throws Exception {
595 setUpBridgeAndThing();
598 getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.ON);
601 waitForAssert(() -> {
602 verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), true);
607 public void testHandleCommandPowerOff() throws Exception {
609 setUpBridgeAndThing();
612 getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.OFF);
615 waitForAssert(() -> {
616 verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), false);
621 public void testHandleCommandDoesNothingWhenPowerCommandIsNotOfOnOffType() throws Exception {
623 setUpBridgeAndThing();
626 getThingHandler().handleCommand(channel(POWER_ON_OFF), new DecimalType(0));
629 verify(getWebserviceMock(), never()).putPowerState(anyString(), anyBoolean());
633 public void testMissingPropertiesAreSetWhenAStateUpdateIsReceivedFromTheCloud() throws Exception {
635 setUpBridgeAndThing();
637 assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_SERIAL_NUMBER));
638 assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_MODEL_ID));
640 var deviceState = mock(DeviceState.class);
641 when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
642 when(deviceState.getDeviceIdentifier()).thenReturn(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
643 when(deviceState.getFabNumber())
644 .thenReturn(Optional.of(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER));
645 when(deviceState.getType()).thenReturn(Optional.of("Unknown device type"));
646 when(deviceState.getTechType()).thenReturn(Optional.of("UK-4567"));
649 getThingHandler().onDeviceStateUpdated(deviceState);
652 assertEquals(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER,
653 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_SERIAL_NUMBER));
654 assertEquals("Unknown device type UK-4567",
655 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_MODEL_ID));