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.discovery;
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.Mockito.*;
17 import static org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants.*;
19 import java.util.List;
20 import java.util.Objects;
21 import java.util.Optional;
22 import java.util.stream.Collectors;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.junit.jupiter.api.BeforeEach;
27 import org.junit.jupiter.api.Test;
28 import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
29 import org.openhab.binding.mielecloud.internal.util.MieleCloudBindingIntegrationTestConstants;
30 import org.openhab.binding.mielecloud.internal.util.OpenHabOsgiTest;
31 import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
32 import org.openhab.binding.mielecloud.internal.webservice.api.json.Device;
33 import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceIdentLabel;
34 import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceType;
35 import org.openhab.binding.mielecloud.internal.webservice.api.json.Ident;
36 import org.openhab.binding.mielecloud.internal.webservice.api.json.Type;
37 import org.openhab.core.config.discovery.DiscoveryResult;
38 import org.openhab.core.config.discovery.DiscoveryService;
39 import org.openhab.core.config.discovery.inbox.InboxPredicates;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingTypeUID;
42 import org.openhab.core.thing.ThingUID;
45 * @author Björn Lange - Initial contribution
48 public class ThingDiscoveryTest extends OpenHabOsgiTest {
49 private static final String DEVICE_TYPE_NAME_COFFEE_SYSTEM = "Coffee System";
50 private static final String DEVICE_TYPE_NAME_DISHWASHER = "Dishwasher";
51 private static final String DEVICE_TYPE_NAME_DISH_WARMER = "Dish Warmer";
52 private static final String DEVICE_TYPE_NAME_DRYER = "Dryer";
53 private static final String DEVICE_TYPE_NAME_FRIDGE_FREEZER = "Fridge Freezer";
54 private static final String DEVICE_TYPE_NAME_HOB = "Hob";
55 private static final String DEVICE_TYPE_NAME_HOOD = "Hood";
56 private static final String DEVICE_TYPE_NAME_OVEN = "Oven";
57 private static final String DEVICE_TYPE_NAME_ROBOTIC_VACUUM_CLEANER = "Robotic Vacuum Cleaner";
58 private static final String DEVICE_TYPE_NAME_WASHING_MACHINE = "Washing Machine";
59 private static final String DEVICE_TYPE_NAME_WINE_STORAGE = "Wine Storage";
61 private static final String TECH_TYPE = "WM1234";
62 private static final String TECH_TYPE_2 = "CM1234";
63 private static final String DEVICE_NAME = "My Device";
64 private static final String DEVICE_NAME_2 = "My Other Device";
65 private static final String SERIAL_NUMBER_2 = "900124430017";
67 private static final ThingUID DISHWASHER_DEVICE_THING_UID_WITH_SERIAL_NUMBER_2 = new ThingUID(
68 new ThingTypeUID(MieleCloudBindingConstants.BINDING_ID, "dishwasher"), BRIDGE_THING_UID, SERIAL_NUMBER_2);
71 private ThingDiscoveryService discoveryService;
73 private ThingDiscoveryService getDiscoveryService() {
74 assertNotNull(discoveryService);
75 return Objects.requireNonNull(discoveryService);
80 throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
82 setUpDiscoveryService();
85 private void setUpDiscoveryService()
86 throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
88 discoveryService = getService(DiscoveryService.class, ThingDiscoveryService.class);
89 assertNotNull(discoveryService);
92 getDiscoveryService().activate();
95 private DeviceState createDeviceState(String fabNumber, String techType, String deviceName, DeviceType deviceType,
96 String deviceTypeText) {
98 DeviceIdentLabel deviceIdentLabel = mock(DeviceIdentLabel.class);
99 when(deviceIdentLabel.getFabNumber()).thenReturn(Optional.of(fabNumber));
100 when(deviceIdentLabel.getTechType()).thenReturn(Optional.of(techType));
102 Type type = mock(Type.class);
103 when(type.getValueRaw()).thenReturn(deviceType);
104 when(type.getValueLocalized()).thenReturn(Optional.of(deviceTypeText));
106 Ident ident = mock(Ident.class);
107 when(ident.getDeviceIdentLabel()).thenReturn(Optional.of(deviceIdentLabel));
108 when(ident.getType()).thenReturn(Optional.of(type));
109 when(ident.getDeviceName()).thenReturn(Optional.of(deviceName));
111 Device device = mock(Device.class);
112 when(device.getIdent()).thenReturn(Optional.of(ident));
114 return new DeviceState(fabNumber, device);
117 private void assertValidDiscoveryResult(ThingUID expectedThingUID, String expectedSerialNumber,
118 String expectedDeviceIdentifier, String expectedLabel, String expectedModelId) {
119 List<DiscoveryResult> results = getInbox().stream().filter(InboxPredicates.forThingUID(expectedThingUID))
120 .collect(Collectors.toList());
121 assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
123 DiscoveryResult result = results.get(0);
124 assertEquals(MieleCloudBindingConstants.BINDING_ID, result.getBindingId(), "Invalid binding ID");
125 assertEquals(MieleCloudBindingIntegrationTestConstants.BRIDGE_THING_UID, result.getBridgeUID(),
126 "Invalid bridge UID");
127 assertEquals(Thing.PROPERTY_SERIAL_NUMBER, result.getRepresentationProperty(),
128 "Invalid representation property");
129 assertEquals(expectedModelId, result.getProperties().get(Thing.PROPERTY_MODEL_ID), "Invalid model ID");
130 assertEquals(expectedLabel, result.getLabel(), "Invalid label");
131 assertEquals(expectedSerialNumber, result.getProperties().get(Thing.PROPERTY_SERIAL_NUMBER),
132 "Invalid serial number");
133 assertEquals(expectedDeviceIdentifier,
134 result.getProperties().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER),
135 "Invalid serial number");
138 private void testMieleDeviceInboxDiscoveryResult(DeviceType deviceType, ThingUID expectedThingUid,
139 String deviceTypeName) {
141 DeviceState deviceState = createDeviceState(SERIAL_NUMBER, TECH_TYPE, DEVICE_NAME, deviceType, deviceTypeName);
144 getDiscoveryService().onDeviceStateUpdated(deviceState);
147 assertValidDiscoveryResult(expectedThingUid, SERIAL_NUMBER, SERIAL_NUMBER, DEVICE_NAME,
148 deviceTypeName + " " + TECH_TYPE);
152 public void testWashingDeviceInboxDiscoveryResult() {
153 testMieleDeviceInboxDiscoveryResult(DeviceType.WASHING_MACHINE, WASHING_MACHINE_THING_UID,
154 DEVICE_TYPE_NAME_WASHING_MACHINE);
158 public void testOvenInboxDiscoveryResult() {
159 testMieleDeviceInboxDiscoveryResult(DeviceType.OVEN, OVEN_DEVICE_THING_UID, DEVICE_TYPE_NAME_OVEN);
163 public void testHobInboxDiscoveryResult() {
164 testMieleDeviceInboxDiscoveryResult(DeviceType.HOB_HIGHLIGHT, HOB_DEVICE_THING_UID, DEVICE_TYPE_NAME_HOB);
168 public void testCoolingDeviceInboxDiscoveryResult() {
169 testMieleDeviceInboxDiscoveryResult(DeviceType.FRIDGE_FREEZER_COMBINATION, FRIDGE_FREEZER_DEVICE_THING_UID,
170 DEVICE_TYPE_NAME_FRIDGE_FREEZER);
174 public void testHoodInboxDiscoveryResult() {
175 testMieleDeviceInboxDiscoveryResult(DeviceType.HOOD, HOOD_DEVICE_THING_UID, DEVICE_TYPE_NAME_HOOD);
179 public void testCoffeeDeviceInboxDiscoveryResult() {
180 testMieleDeviceInboxDiscoveryResult(DeviceType.COFFEE_SYSTEM, COFFEE_SYSTEM_THING_UID,
181 DEVICE_TYPE_NAME_COFFEE_SYSTEM);
185 public void testWineStorageDeviceInboxDiscoveryResult() {
186 testMieleDeviceInboxDiscoveryResult(DeviceType.WINE_CABINET, WINE_STORAGE_DEVICE_THING_UID,
187 DEVICE_TYPE_NAME_WINE_STORAGE);
191 public void testDryerInboxDiscoveryResult() {
192 testMieleDeviceInboxDiscoveryResult(DeviceType.TUMBLE_DRYER, DRYER_DEVICE_THING_UID, DEVICE_TYPE_NAME_DRYER);
196 public void testDishwasherInboxDiscoveryResult() {
197 testMieleDeviceInboxDiscoveryResult(DeviceType.DISHWASHER, DISHWASHER_DEVICE_THING_UID,
198 DEVICE_TYPE_NAME_DISHWASHER);
202 public void testDishWarmerInboxDiscoveryResult() {
203 testMieleDeviceInboxDiscoveryResult(DeviceType.DISH_WARMER, DISH_WARMER_DEVICE_THING_UID,
204 DEVICE_TYPE_NAME_DISH_WARMER);
208 public void testRoboticVacuumCleanerInboxDiscoveryResult() {
209 testMieleDeviceInboxDiscoveryResult(DeviceType.VACUUM_CLEANER, ROBOTIC_VACUUM_CLEANER_THING_UID,
210 DEVICE_TYPE_NAME_ROBOTIC_VACUUM_CLEANER);
214 public void testUnknownDeviceCreatesNoInboxDiscoveryResult() {
216 DeviceState deviceState = createDeviceState(SERIAL_NUMBER, TECH_TYPE, DEVICE_NAME, DeviceType.VACUUM_DRAWER,
220 getDiscoveryService().onDeviceStateUpdated(deviceState);
223 waitForAssert(() -> {
224 List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
225 assertEquals(0, results.size(), "Amount of things in inbox does not match expected number");
230 public void testDeviceDiscoveryResultOfDeviceRemovedInTheCloudIsRemovedFromTheInbox() throws InterruptedException {
232 testMieleDeviceInboxDiscoveryResult(DeviceType.HOOD, HOOD_DEVICE_THING_UID, DEVICE_TYPE_NAME_HOOD);
237 getDiscoveryService().onDeviceRemoved(SERIAL_NUMBER);
240 waitForAssert(() -> {
241 List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
242 assertEquals(0, results.size(), "Amount of things in inbox does not match expected number");
247 public void testDiscoveryResultsForTwoDevices() {
249 DeviceState hoodDevice = createDeviceState(SERIAL_NUMBER, TECH_TYPE, DEVICE_NAME, DeviceType.HOOD,
250 DEVICE_TYPE_NAME_HOOD);
251 DeviceState dishwasherDevice = createDeviceState(SERIAL_NUMBER_2, TECH_TYPE_2, DEVICE_NAME_2,
252 DeviceType.DISHWASHER, DEVICE_TYPE_NAME_DISHWASHER);
255 getDiscoveryService().onDeviceStateUpdated(hoodDevice);
256 getDiscoveryService().onDeviceStateUpdated(dishwasherDevice);
259 waitForAssert(() -> {
260 List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
261 assertEquals(2, results.size(), "Amount of things in inbox does not match expected number");
263 assertValidDiscoveryResult(HOOD_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER, DEVICE_NAME,
264 "Hood " + TECH_TYPE);
265 assertValidDiscoveryResult(DISHWASHER_DEVICE_THING_UID_WITH_SERIAL_NUMBER_2, SERIAL_NUMBER_2,
266 SERIAL_NUMBER_2, DEVICE_NAME_2, DEVICE_TYPE_NAME_DISHWASHER + " " + TECH_TYPE_2);
271 public void testOnlyDeviceDiscoveryResultsOfDevicesRemovedInTheCloudAreRemovedFromTheInbox()
272 throws InterruptedException {
274 DeviceState hoodDevice = createDeviceState(SERIAL_NUMBER, TECH_TYPE, DEVICE_NAME, DeviceType.HOOD,
275 DEVICE_TYPE_NAME_HOOD);
276 DeviceState dishwasherDevice = createDeviceState(SERIAL_NUMBER_2, TECH_TYPE_2, DEVICE_NAME_2,
277 DeviceType.DISHWASHER, DEVICE_TYPE_NAME_DISHWASHER);
278 getDiscoveryService().onDeviceStateUpdated(hoodDevice);
279 getDiscoveryService().onDeviceStateUpdated(dishwasherDevice);
284 // This order of invocation is enforced by the webservice implementation.
285 getDiscoveryService().onDeviceRemoved(SERIAL_NUMBER_2);
286 getDiscoveryService().onDeviceStateUpdated(hoodDevice);
289 waitForAssert(() -> {
290 List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
291 assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
293 assertValidDiscoveryResult(HOOD_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER, DEVICE_NAME,
294 DEVICE_TYPE_NAME_HOOD + " " + TECH_TYPE);
299 public void testIfNoDeviceNameIsSetThenTheDiscoveryLabelIsTheDeviceTypePlusTheTechType() {
301 DeviceState deviceState = createDeviceState(SERIAL_NUMBER, TECH_TYPE, "", DeviceType.FRIDGE_FREEZER_COMBINATION,
302 DEVICE_TYPE_NAME_FRIDGE_FREEZER);
305 getDiscoveryService().onDeviceStateUpdated(deviceState);
308 waitForAssert(() -> {
309 List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
310 assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
312 assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER,
313 "Fridge Freezer " + TECH_TYPE, DEVICE_TYPE_NAME_FRIDGE_FREEZER + " " + TECH_TYPE);
318 public void testIfNeitherDeviceTypeNorDeviceNameAreSetThenTheDiscoveryModelIdAndTheLabelAreTheTechType() {
320 DeviceState deviceState = createDeviceState(SERIAL_NUMBER, TECH_TYPE, "", DeviceType.FRIDGE_FREEZER_COMBINATION,
324 getDiscoveryService().onDeviceStateUpdated(deviceState);
327 waitForAssert(() -> {
328 List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
329 assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
331 assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER, TECH_TYPE,
337 public void testIfNeitherTechTypeNorDeviceNameAreSetThenTheDiscoveryModelIdAndTheLabelAreTheDeviceType() {
339 DeviceState deviceState = createDeviceState(SERIAL_NUMBER, "", "", DeviceType.FRIDGE_FREEZER_COMBINATION,
340 DEVICE_TYPE_NAME_FRIDGE_FREEZER);
343 getDiscoveryService().onDeviceStateUpdated(deviceState);
346 waitForAssert(() -> {
347 List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
348 assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
350 assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER,
351 DEVICE_TYPE_NAME_FRIDGE_FREEZER, DEVICE_TYPE_NAME_FRIDGE_FREEZER);
356 public void testIfNeitherTechTypeNorDeviceTypeNorDeviceNameAreSetThenTheDiscoveryModelIdIsUnknownAndTheLabelIsMieleDevice() {
358 DeviceState deviceState = createDeviceState(SERIAL_NUMBER, "", "", DeviceType.FRIDGE_FREEZER_COMBINATION, "");
361 getDiscoveryService().onDeviceStateUpdated(deviceState);
364 waitForAssert(() -> {
365 List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
366 assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
368 assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER, "Miele Device",
374 public void testIfNoSerialNumberIsSetThenTheDeviceIdentifierIsUsedAsReplacement() {
376 DeviceIdentLabel deviceIdentLabel = mock(DeviceIdentLabel.class);
377 when(deviceIdentLabel.getFabNumber()).thenReturn(Optional.of(""));
378 when(deviceIdentLabel.getTechType()).thenReturn(Optional.of(TECH_TYPE));
380 Type type = mock(Type.class);
381 when(type.getValueRaw()).thenReturn(DeviceType.FRIDGE_FREEZER_COMBINATION);
382 when(type.getValueLocalized()).thenReturn(Optional.of(DEVICE_TYPE_NAME_FRIDGE_FREEZER));
384 Ident ident = mock(Ident.class);
385 when(ident.getDeviceIdentLabel()).thenReturn(Optional.of(deviceIdentLabel));
386 when(ident.getType()).thenReturn(Optional.of(type));
387 when(ident.getDeviceName()).thenReturn(Optional.of(""));
389 Device device = mock(Device.class);
390 when(device.getIdent()).thenReturn(Optional.of(ident));
391 DeviceState deviceState = new DeviceState(SERIAL_NUMBER, device);
394 getDiscoveryService().onDeviceStateUpdated(deviceState);
397 waitForAssert(() -> {
398 List<DiscoveryResult> results = getInbox().stream().collect(Collectors.toList());
399 assertEquals(1, results.size(), "Amount of things in inbox does not match expected number");
401 assertValidDiscoveryResult(FRIDGE_FREEZER_DEVICE_THING_UID, SERIAL_NUMBER, SERIAL_NUMBER,
402 DEVICE_TYPE_NAME_FRIDGE_FREEZER + " " + TECH_TYPE,
403 DEVICE_TYPE_NAME_FRIDGE_FREEZER + " " + TECH_TYPE);