2 * Copyright (c) 2010-2024 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.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")
184 .withConfiguration(new Configuration(Map.of(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 String thingTypeVersion) {
213 ThingRegistry registry = getThingRegistry();
215 List<Channel> channels = createChannelsForThingHandler(thingTypeUid, thingUid);
217 Thing thing = ThingBuilder.create(thingTypeUid, thingUid)
218 .withConfiguration(new Configuration(
219 Map.of(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER, deviceIdentifier)))
220 .withBridge(getBridge().getUID()).withChannels(channels).withLabel("DA-6996")
221 .withProperty("thingTypeVersion", thingTypeVersion).build();
222 assertNotNull(thing);
226 waitForAssert(() -> {
227 ThingHandler handler = thing.getHandler();
228 assertNotNull(handler);
229 assertTrue(expectedHandlerClass.isAssignableFrom(handler.getClass()), "Handler type is wrong");
232 createItemsForChannels(thing);
233 linkChannelsToItems(thing);
235 ThingHandler handler = thing.getHandler();
236 assertNotNull(handler);
237 AbstractMieleThingHandler mieleThingHandler = (AbstractMieleThingHandler) Objects.requireNonNull(handler);
239 waitForAssert(() -> {
241 assertNotNull(ReflectionUtil.invokePrivate(mieleThingHandler, "getBridge"));
242 } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) {
243 throw new RuntimeException(e);
245 assertNotNull(getBridge().getThing(thingUid));
248 return mieleThingHandler;
251 private List<Channel> createChannelsForThingHandler(ThingTypeUID thingTypeUid, ThingUID thingUid) {
252 ChannelTypeRegistry channelTypeRegistry = getService(ChannelTypeRegistry.class, ChannelTypeRegistry.class);
253 assertNotNull(channelTypeRegistry);
255 ThingTypeRegistry thingTypeRegistry = getService(ThingTypeRegistry.class, ThingTypeRegistry.class);
256 assertNotNull(thingTypeRegistry);
258 ThingType thingType = thingTypeRegistry.getThingType(thingTypeUid);
259 assertNotNull(thingType);
261 List<ChannelDefinition> channelDefinitions = thingType.getChannelDefinitions();
262 assertNotNull(channelDefinitions);
264 List<Channel> channels = new ArrayList<Channel>();
265 for (ChannelDefinition channelDefinition : channelDefinitions) {
266 ChannelTypeUID channelTypeUid = channelDefinition.getChannelTypeUID();
267 assertNotNull(channelTypeUid);
269 ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUid);
270 assertNotNull(channelType);
272 String acceptedItemType = channelType.getItemType();
273 assertNotNull(acceptedItemType);
275 String channelId = channelDefinition.getId();
276 assertNotNull(channelId);
278 ChannelUID channelUid = new ChannelUID(thingUid, channelId);
279 assertNotNull(channelUid);
281 Channel channel = ChannelBuilder.create(channelUid, acceptedItemType).build();
282 assertNotNull(channel);
284 channels.add(channel);
290 private void createItemsForChannels(Thing thing) {
291 ItemBuilderFactory itemBuilderFactory = getService(ItemBuilderFactory.class);
292 assertNotNull(itemBuilderFactory);
294 for (Channel channel : thing.getChannels()) {
295 String acceptedItemType = channel.getAcceptedItemType();
296 assertNotNull(acceptedItemType);
298 ItemBuilder itemBuilder = itemBuilderFactory.newItemBuilder(Objects.requireNonNull(acceptedItemType),
299 channel.getUID().getId());
300 assertNotNull(itemBuilder);
302 Item item = itemBuilder.build();
305 getItemRegistry().add(item);
309 private void linkChannelsToItems(Thing thing) {
310 ItemChannelLinkRegistry itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class,
311 ItemChannelLinkRegistry.class);
312 assertNotNull(itemChannelLinkRegistry);
314 for (Channel channel : thing.getChannels()) {
315 String itemName = channel.getUID().getId();
316 assertNotNull(itemName);
318 ChannelUID channelUid = channel.getUID();
319 assertNotNull(channelUid);
321 ItemChannelLink link = itemChannelLinkRegistry.add(new ItemChannelLink(itemName, channelUid));
326 protected ChannelUID channel(String id) {
327 return new ChannelUID(getThingHandler().getThing().getUID(), id);
331 public void setUpAbstractMieleThingHandlerTest() throws Exception {
332 registerVolatileStorageService();
333 setUpThingRegistry();
338 protected void setUpBridgeAndThing() throws Exception {
340 thingHandler = setUpThingHandler();
343 private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail) {
344 assertThingStatusIs(thing, expectedStatus, expectedStatusDetail, null);
347 private void assertThingStatusIs(Thing thing, ThingStatus expectedStatus, ThingStatusDetail expectedStatusDetail,
348 @Nullable String expectedDescription) {
349 assertEquals(expectedStatus, thing.getStatus());
350 assertEquals(expectedStatusDetail, thing.getStatusInfo().getStatusDetail());
351 if (expectedDescription == null) {
352 assertNull(thing.getStatusInfo().getDescription());
354 assertEquals(expectedDescription, thing.getStatusInfo().getDescription());
358 protected State getChannelState(String channelUid) {
359 Item item = getItemRegistry().get(channelUid);
360 assertNotNull(item, "Item for channel UID " + channelUid + " is null.");
361 return item.getState();
365 * Sets up the {@link ThingHandler} under test.
367 * @return The created {@link ThingHandler}.
369 protected abstract AbstractMieleThingHandler setUpThingHandler();
372 public void tearDownAbstractMieleThingHandlerTest() {
373 getThingRegistry().forceRemove(getThingHandler().getThing().getUID());
374 getThingRegistry().forceRemove(getBridge().getUID());
378 public void testCachedStateIsQueriedOnInitialize() throws Exception {
380 setUpBridgeAndThing();
383 verify(getWebserviceMock()).dispatchDeviceState(SERIAL_NUMBER);
387 public void testThingStatusIsOfflineWithDetailGoneAndDetailMessageWhenDeviceIsRemoved() throws Exception {
389 setUpBridgeAndThing();
392 getBridgeHandler().onDeviceRemoved(SERIAL_NUMBER);
395 Thing thing = getThingHandler().getThing();
396 assertThingStatusIs(thing, ThingStatus.OFFLINE, ThingStatusDetail.GONE,
397 "@text/mielecloud.thing.status.removed");
400 private DeviceState createDeviceStateMock(StateType stateType, String localizedState) {
401 DeviceState deviceState = mock(DeviceState.class);
402 when(deviceState.getDeviceIdentifier()).thenReturn(getThingHandler().getThing().getUID().getId());
403 when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
404 when(deviceState.getStateType()).thenReturn(Optional.of(stateType));
405 when(deviceState.isInState(any())).thenCallRealMethod();
406 when(deviceState.getStatus()).thenReturn(Optional.of(localizedState));
411 public void testStatusIsSetToOnlineWhenDeviceStateIsValid() throws Exception {
413 setUpBridgeAndThing();
415 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
418 getBridgeHandler().onDeviceStateUpdated(deviceState);
421 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
425 public void testStatusIsSetToOfflineWhenDeviceIsNotConnected() throws Exception {
427 setUpBridgeAndThing();
429 DeviceState deviceState = createDeviceStateMock(StateType.NOT_CONNECTED, "Not connected");
432 getBridgeHandler().onDeviceStateUpdated(deviceState);
435 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
436 "@text/mielecloud.thing.status.disconnected");
440 public void testFailingPutProcessActionDoesNotSetTheDeviceToOffline() throws Exception {
442 doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putProcessAction(any(),
443 eq(ProcessAction.STOP));
445 setUpBridgeAndThing();
447 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
448 getBridgeHandler().onDeviceStateUpdated(deviceState);
449 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
452 getThingHandler().triggerProcessAction(ProcessAction.STOP);
455 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
459 public void testHandleCommandProgramStartToStartStopChannel() throws Exception {
461 setUpBridgeAndThing();
464 getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
465 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
468 waitForAssert(() -> {
469 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
474 public void testHandleCommandProgramStopToStartStopChannel() throws Exception {
476 setUpBridgeAndThing();
479 getThingHandler().handleCommand(channel(PROGRAM_START_STOP),
480 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
483 waitForAssert(() -> {
484 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
489 public void testHandleCommandProgramStartToStartStopPauseChannel() throws Exception {
491 setUpBridgeAndThing();
494 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
495 new StringType(ProgramStatus.PROGRAM_STARTED.getState()));
498 waitForAssert(() -> {
499 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.START);
504 public void testHandleCommandProgramStopToStartStopPauseChannel() throws Exception {
506 setUpBridgeAndThing();
509 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
510 new StringType(ProgramStatus.PROGRAM_STOPPED.getState()));
513 waitForAssert(() -> {
514 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.STOP);
519 public void testHandleCommandProgramPauseToStartStopPauseChannel() throws Exception {
521 setUpBridgeAndThing();
524 getThingHandler().handleCommand(channel(PROGRAM_START_STOP_PAUSE),
525 new StringType(ProgramStatus.PROGRAM_PAUSED.getState()));
528 waitForAssert(() -> {
529 verify(getWebserviceMock()).putProcessAction(getThingHandler().getDeviceId(), ProcessAction.PAUSE);
534 public void testFailingPutLightDoesNotSetTheDeviceToOffline() throws Exception {
536 doThrow(MieleWebserviceException.class).when(getWebserviceMock()).putLight(any(), eq(true));
538 setUpBridgeAndThing();
540 DeviceState deviceState = createDeviceStateMock(StateType.ON, "On");
541 getBridgeHandler().onDeviceStateUpdated(deviceState);
542 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
545 getThingHandler().triggerLight(true);
548 assertThingStatusIs(getThingHandler().getThing(), ThingStatus.ONLINE, ThingStatusDetail.NONE);
552 public void testHandleCommandLightOff() throws Exception {
554 setUpBridgeAndThing();
557 getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.OFF);
560 waitForAssert(() -> {
561 verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), false);
566 public void testHandleCommandLightOn() throws Exception {
568 setUpBridgeAndThing();
571 getThingHandler().handleCommand(channel(LIGHT_SWITCH), OnOffType.ON);
574 waitForAssert(() -> {
575 verify(getWebserviceMock()).putLight(getThingHandler().getDeviceId(), true);
580 public void testHandleCommandDoesNothingWhenCommandIsNotOfOnOffType() throws Exception {
582 setUpBridgeAndThing();
585 getThingHandler().handleCommand(channel(LIGHT_SWITCH), new DecimalType(0));
588 verify(getWebserviceMock(), never()).putLight(anyString(), anyBoolean());
592 public void testHandleCommandPowerOn() throws Exception {
594 setUpBridgeAndThing();
597 getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.ON);
600 waitForAssert(() -> {
601 verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), true);
606 public void testHandleCommandPowerOff() throws Exception {
608 setUpBridgeAndThing();
611 getThingHandler().handleCommand(channel(POWER_ON_OFF), OnOffType.OFF);
614 waitForAssert(() -> {
615 verify(getWebserviceMock()).putPowerState(getThingHandler().getDeviceId(), false);
620 public void testHandleCommandDoesNothingWhenPowerCommandIsNotOfOnOffType() throws Exception {
622 setUpBridgeAndThing();
625 getThingHandler().handleCommand(channel(POWER_ON_OFF), new DecimalType(0));
628 verify(getWebserviceMock(), never()).putPowerState(anyString(), anyBoolean());
632 public void testMissingPropertiesAreSetWhenAStateUpdateIsReceivedFromTheCloud() throws Exception {
634 setUpBridgeAndThing();
636 assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_SERIAL_NUMBER));
637 assertFalse(getThingHandler().getThing().getProperties().containsKey(Thing.PROPERTY_MODEL_ID));
639 var deviceState = mock(DeviceState.class);
640 when(deviceState.getRawType()).thenReturn(DeviceType.UNKNOWN);
641 when(deviceState.getDeviceIdentifier()).thenReturn(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER);
642 when(deviceState.getFabNumber())
643 .thenReturn(Optional.of(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER));
644 when(deviceState.getType()).thenReturn(Optional.of("Unknown device type"));
645 when(deviceState.getTechType()).thenReturn(Optional.of("UK-4567"));
648 getThingHandler().onDeviceStateUpdated(deviceState);
651 assertEquals(MieleCloudBindingIntegrationTestConstants.SERIAL_NUMBER,
652 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_SERIAL_NUMBER));
653 assertEquals("Unknown device type UK-4567",
654 getThingHandler().getThing().getProperties().get(Thing.PROPERTY_MODEL_ID));