* [boschshc] add command to list Bosch Smart Home Controller devices and mapping to openhab devices and related services
Signed-off-by: Gerd Zanker <gerd.zanker@web.de>
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.console;
+
+import static org.openhab.binding.boschshc.internal.discovery.ThingDiscoveryService.DEVICEMODEL_TO_THINGTYPE_MAP;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
+import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.core.io.console.Console;
+import org.openhab.core.io.console.ConsoleCommandCompleter;
+import org.openhab.core.io.console.StringsCompleter;
+import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
+import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Console command to list Bosch SHC devices and openhab support.
+ * Use the SHC API to get all SHC devices and SHC services
+ * and tries to lookup openhab devices and implemented service classes.
+ * Prints each name and looked-up implementation on console.
+ *
+ * @author Gerd Zanker - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = ConsoleCommandExtension.class)
+public class BoschShcCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter {
+
+ static final String SHOW_BINDINGINFO = "showBindingInfo";
+ static final String SHOW_DEVICES = "showDevices";
+ static final String SHOW_SERVICES = "showServices";
+
+ static final String GET_BRIDGEINFO = "bridgeInfo";
+ static final String GET_DEVICES = "deviceInfo";
+ private static final StringsCompleter SUBCMD_COMPLETER = new StringsCompleter(
+ List.of(SHOW_BINDINGINFO, SHOW_DEVICES, SHOW_SERVICES, GET_BRIDGEINFO, GET_DEVICES), false);
+
+ private final ThingRegistry thingRegistry;
+
+ @Activate
+ public BoschShcCommandExtension(final @Reference ThingRegistry thingRegistry) {
+ super(BoschSHCBindingConstants.BINDING_ID, "Interact with the Bosch Smart Home Controller.");
+ this.thingRegistry = thingRegistry;
+ }
+
+ /**
+ * Returns all implemented services of this Bosch SHC binding.
+ * This list shall contain all available services and needs to be extended when a new service is added.
+ * A unit tests checks if this list matches with the existing subfolders in
+ * "src/main/java/org/openhab/binding/boschshc/internal/services".
+ */
+ List<String> getAllBoschShcServices() {
+ return List.of("airqualitylevel", "batterylevel", "binaryswitch", "bypass", "cameranotification", "childlock",
+ "communicationquality", "hsbcoloractuator", "humiditylevel", "illuminance", "intrusion", "keypad",
+ "latestmotion", "multilevelswitch", "powermeter", "powerswitch", "privacymode", "roomclimatecontrol",
+ "shuttercontact", "shuttercontrol", "silentmode", "smokedetectorcheck", "temperaturelevel", "userstate",
+ "valvetappet");
+ }
+
+ @Override
+ public void execute(String[] args, Console console) {
+ if (args.length == 0) {
+ printUsage(console);
+ return;
+ }
+ try {
+ if (GET_BRIDGEINFO.equals(args[0])) {
+ console.print(buildBridgeInfo());
+ return;
+ }
+ if (GET_DEVICES.equals(args[0])) {
+ console.print(buildDeviceInfo());
+ return;
+ }
+ if (SHOW_BINDINGINFO.equals(args[0])) {
+ console.print(buildBindingInfo());
+ return;
+ }
+ if (SHOW_DEVICES.equals(args[0])) {
+ console.print(buildSupportedDeviceStatus());
+ return;
+ }
+ if (SHOW_SERVICES.equals(args[0])) {
+ console.print(buildSupportedServiceStatus());
+ return;
+ }
+ } catch (BoschSHCException | ExecutionException | TimeoutException e) {
+ console.print(String.format("Error %1s%n", e.getMessage()));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ // unsupported command, print usage
+ printUsage(console);
+ }
+
+ private List<BridgeHandler> getBridgeHandlers() {
+ List<BridgeHandler> bridges = new ArrayList<>();
+ for (Thing thing : thingRegistry.getAll()) {
+ ThingHandler thingHandler = thing.getHandler();
+ if (thingHandler instanceof BridgeHandler bridgeHandler) {
+ bridges.add(bridgeHandler);
+ }
+ }
+ return bridges;
+ }
+
+ String buildBridgeInfo() throws BoschSHCException, InterruptedException, ExecutionException, TimeoutException {
+ List<BridgeHandler> bridges = getBridgeHandlers();
+ StringBuilder builder = new StringBuilder();
+ for (BridgeHandler bridgeHandler : bridges) {
+ builder.append(String.format("Bridge: %1s%n", bridgeHandler.getThing().getLabel()));
+ builder.append(String.format(" access possible: %1s%n", bridgeHandler.checkBridgeAccess()));
+
+ PublicInformation publicInformation = bridgeHandler.getPublicInformation();
+ builder.append(String.format(" SHC Generation: %1s%n", publicInformation.shcGeneration));
+ builder.append(String.format(" IP Address: %1s%n", publicInformation.shcIpAddress));
+ builder.append(String.format(" API Versions: %1s%n", publicInformation.apiVersions));
+ builder.append(String.format(" Software Version: %1s%n",
+ publicInformation.softwareUpdateState.swInstalledVersion));
+ builder.append(String.format(" Version Update State: %1s%n",
+ publicInformation.softwareUpdateState.swUpdateState));
+ builder.append(String.format(" Available Version: %1s%n",
+ publicInformation.softwareUpdateState.swUpdateAvailableVersion));
+ builder.append(String.format("%n"));
+ }
+ return builder.toString();
+ }
+
+ String buildDeviceInfo() throws InterruptedException {
+ StringBuilder builder = new StringBuilder();
+ for (Thing thing : thingRegistry.getAll()) {
+ ThingHandler thingHandler = thing.getHandler();
+ if (thingHandler instanceof BridgeHandler bridgeHandler) {
+ builder.append(String.format("thing: %1s%n", thing.getLabel()));
+ builder.append(String.format(" thingHandler: %1s%n", thingHandler.getClass().getName()));
+ builder.append(String.format("bridge access possible: %1s%n", bridgeHandler.checkBridgeAccess()));
+
+ List<Device> devices = bridgeHandler.getDevices();
+ builder.append(String.format("devices (%1d): %n", devices.size()));
+ for (Device device : devices) {
+ builder.append(buildDeviceInfo(device));
+ builder.append(String.format("%n"));
+ }
+ }
+ }
+ return builder.toString();
+ }
+
+ private String buildDeviceInfo(Device device) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(String.format(" deviceID: %1s%n", device.id));
+ builder.append(String.format(" type: %1s -> ", device.deviceModel));
+ if (DEVICEMODEL_TO_THINGTYPE_MAP.containsKey(device.deviceModel)) {
+ builder.append(DEVICEMODEL_TO_THINGTYPE_MAP.get(device.deviceModel).getId());
+ } else {
+ builder.append("!UNSUPPORTED!");
+ }
+ builder.append(String.format("%n"));
+
+ builder.append(buildDeviceServices(device.deviceServiceIds));
+ return builder.toString();
+ }
+
+ private String buildDeviceServices(List<String> deviceServiceIds) {
+ StringBuilder builder = new StringBuilder();
+ List<String> existingServices = getAllBoschShcServices();
+ for (String serviceName : deviceServiceIds) {
+ builder.append(String.format(" service: %1s -> ", serviceName));
+
+ if (existingServices.stream().anyMatch(s -> s.equals(serviceName.toLowerCase()))) {
+ for (String existingService : existingServices) {
+ if (existingService.equals(serviceName.toLowerCase())) {
+ builder.append(existingService);
+ }
+ }
+ } else {
+ builder.append("!UNSUPPORTED!");
+ }
+ builder.append(String.format("%n"));
+ }
+ return builder.toString();
+ }
+
+ String buildBindingInfo() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(String.format("Bosch SHC Binding%n"));
+ Bundle bundle = FrameworkUtil.getBundle(getClass());
+ if (bundle != null) {
+ builder.append(String.format(" SymbolicName %1s%n", bundle.getSymbolicName()));
+ builder.append(String.format(" Version %1s%n", bundle.getVersion()));
+ }
+ return builder.toString();
+ }
+
+ String buildSupportedDeviceStatus() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(String.format("Supported Devices (%1d):%n", DEVICEMODEL_TO_THINGTYPE_MAP.size()));
+ for (Map.Entry<String, ThingTypeUID> entry : DEVICEMODEL_TO_THINGTYPE_MAP.entrySet()) {
+ builder.append(
+ String.format(" - %1s = %1s%n", entry.getKey(), DEVICEMODEL_TO_THINGTYPE_MAP.get(entry.getKey())));
+ }
+ return builder.toString();
+ }
+
+ String buildSupportedServiceStatus() {
+ StringBuilder builder = new StringBuilder();
+ List<String> supportedServices = getAllBoschShcServices();
+ builder.append(String.format("Supported Services (%1d):%n", supportedServices.size()));
+ for (String service : supportedServices) {
+ builder.append(String.format(" - %1s%n", service));
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public List<String> getUsages() {
+ return List.of(buildCommandUsage(SHOW_BINDINGINFO, "list detailed information about this binding"),
+ buildCommandUsage(SHOW_DEVICES, "list all devices supported by this binding"),
+ buildCommandUsage(SHOW_SERVICES, "list all services supported by this binding"),
+ buildCommandUsage(GET_DEVICES, "get all Bosch SHC devices"),
+ buildCommandUsage(GET_BRIDGEINFO, "get detailed information from Bosch SHC"));
+ }
+
+ @Override
+ public @Nullable ConsoleCommandCompleter getCompleter() {
+ return this;
+ }
+
+ @Override
+ public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
+ if (cursorArgumentIndex <= 0) {
+ return SUBCMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
+ }
+ return false;
+ }
+}
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
}
}
+ /**
+ * Get public information from Bosch SHC.
+ */
+ public PublicInformation getPublicInformation()
+ throws InterruptedException, BoschSHCException, ExecutionException, TimeoutException {
+ @Nullable
+ BoschHttpClient localHttpClient = this.httpClient;
+ if (localHttpClient == null) {
+ throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
+ }
+
+ String url = localHttpClient.getPublicInformationUrl();
+ Request request = localHttpClient.createRequest(url, GET);
+
+ return localHttpClient.sendRequest(request, PublicInformation.class, PublicInformation::isValid, null);
+ }
+
public boolean registerDiscoveryListener(ThingDiscoveryService listener) {
if (thingDiscoveryService == null) {
thingDiscoveryService = listener;
@Nullable
BoschHttpClient localHttpClient = this.httpClient;
if (localHttpClient == null) {
- throw new BoschSHCException("HTTP client not initialized");
+ throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
}
String url = localHttpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId));
@Nullable
BoschHttpClient locaHttpClient = this.httpClient;
if (locaHttpClient == null) {
- throw new BoschSHCException("HTTP client not initialized");
+ throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
}
String url = locaHttpClient.getBoschSmartHomeUrl(String.format("userdefinedstates/%s", stateId));
public String status;
public List<String> childDeviceIds;
- public static Boolean isValid(Device obj) {
+ public static boolean isValid(Device obj) {
return obj != null && obj.id != null;
}
public List<String> apiVersions;
public String shcIpAddress;
public String shcGeneration;
+ public SoftwareUpdateState softwareUpdateState;
+
+ public static boolean isValid(PublicInformation obj) {
+ return obj != null && obj.shcIpAddress != null && obj.shcGeneration != null && obj.apiVersions != null
+ && SoftwareUpdateState.isValid(obj.softwareUpdateState);
+ }
}
return scenario;
}
- public static Boolean isValid(Scenario[] scenarios) {
+ public static boolean isValid(Scenario[] scenarios) {
return Arrays.stream(scenarios).allMatch(scenario -> (scenario.id != null));
}
@Override
public String toString() {
- final StringBuilder sb = new StringBuilder("Scenario{");
- sb.append("name='").append(name).append("'");
- sb.append(", id='").append(id).append("'");
- sb.append(", lastTimeTriggered='").append(lastTimeTriggered).append("'");
- sb.append('}');
- return sb.toString();
+ return "Scenario{" + "name='" + name + "'" + ", id='" + id + "'" + ", lastTimeTriggered='" + lastTimeTriggered
+ + "'" + '}';
}
}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices.bridge.dto;
+
+/**
+ * Software Update State is part of PublicInformation.
+ *
+ * @author Gerd Zanker - Initial contribution
+ */
+public class SoftwareUpdateState {
+
+ public String swUpdateState;
+ public String swInstalledVersion;
+ public String swUpdateAvailableVersion;
+
+ public static boolean isValid(SoftwareUpdateState obj) {
+ return obj != null && obj.swUpdateState != null && obj.swInstalledVersion != null
+ && obj.swUpdateAvailableVersion != null;
+ }
+}
return this.jsonrpc;
}
- public static Boolean isValid(SubscribeResult obj) {
+ public static boolean isValid(SubscribeResult obj) {
return obj != null && obj.result != null && obj.jsonrpc != null;
}
}
+ type + '\'' + '}';
}
- public static Boolean isValid(UserDefinedState obj) {
+ public static boolean isValid(UserDefinedState obj) {
return obj != null && obj.id != null;
}
}
BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR);
// @formatter:off
- protected static final Map<String, ThingTypeUID> DEVICEMODEL_TO_THINGTYPE_MAP = Map.ofEntries(
+ public static final Map<String, ThingTypeUID> DEVICEMODEL_TO_THINGTYPE_MAP = Map.ofEntries(
new AbstractMap.SimpleEntry<>("BBL", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL),
new AbstractMap.SimpleEntry<>("TWINGUARD", BoschSHCBindingConstants.THING_TYPE_TWINGUARD),
new AbstractMap.SimpleEntry<>("BSM", BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH),
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.console;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.SoftwareUpdateState;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.core.io.console.Console;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+
+/**
+ * Unit tests for Console command to list Bosch SHC devices and openhab support.
+ *
+ * @author Gerd Zanker - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+class BoschShcCommandExtensionTest {
+
+ private @NonNullByDefault({}) BoschShcCommandExtension fixture;
+
+ private @Mock @NonNullByDefault({}) ThingRegistry thingRegistry;
+
+ @BeforeEach
+ void setUp() {
+ fixture = new BoschShcCommandExtension(thingRegistry);
+ }
+
+ @Test
+ void execute() {
+ // only sanity checks, content is tested with the functions called by execute
+ Console consoleMock = mock(Console.class);
+ when(thingRegistry.getAll()).thenReturn(Collections.emptyList());
+
+ fixture.execute(new String[] {}, consoleMock);
+ verify(consoleMock, times(5)).printUsage(any());
+ fixture.execute(new String[] { "" }, consoleMock);
+ verify(consoleMock, times(10)).printUsage(any());
+
+ fixture.execute(new String[] { BoschShcCommandExtension.SHOW_BINDINGINFO }, consoleMock);
+ verify(consoleMock, atLeastOnce()).print(any());
+ fixture.execute(new String[] { BoschShcCommandExtension.SHOW_DEVICES }, consoleMock);
+ verify(consoleMock, atLeastOnce()).print(any());
+ fixture.execute(new String[] { BoschShcCommandExtension.SHOW_SERVICES }, consoleMock);
+ verify(consoleMock, atLeastOnce()).print(any());
+
+ fixture.execute(new String[] { BoschShcCommandExtension.GET_BRIDGEINFO }, consoleMock);
+ verify(consoleMock, atLeastOnce()).print(any());
+ fixture.execute(new String[] { BoschShcCommandExtension.GET_DEVICES }, consoleMock);
+ verify(consoleMock, atLeastOnce()).print(any());
+ }
+
+ @Test
+ void getCompleter() {
+ assertThat(fixture.getCompleter(), is(fixture));
+ }
+
+ @Test
+ void getUsages() {
+ List<String> strings = fixture.getUsages();
+ assertThat(strings.size(), is(5));
+ assertThat(strings.get(0), is("boschshc showBindingInfo - list detailed information about this binding"));
+ assertThat(strings.get(1), is("boschshc showDevices - list all devices supported by this binding"));
+ }
+
+ @Test
+ void complete() {
+ ArrayList<String> candidates = new ArrayList<>();
+ assertThat(fixture.complete(new String[] { "" }, 1, 0, candidates), is(false));
+ assertThat(fixture.complete(new String[] { "" }, 0, 0, candidates), is(true));
+ // for empty arguments, the completer suggest all usage commands
+ assertThat(candidates.size(), is(fixture.getUsages().size()));
+ }
+
+ @Test
+ void printBridgeInfo() throws BoschSHCException, ExecutionException, InterruptedException, TimeoutException {
+ // no bridge
+ when(thingRegistry.getAll()).thenReturn(Collections.emptyList());
+ assertThat(fixture.buildBridgeInfo(), is(""));
+
+ // one bridge
+ PublicInformation publicInformation = new PublicInformation();
+ publicInformation.shcGeneration = "Gen-T";
+ publicInformation.shcIpAddress = "1.2.3.4";
+ publicInformation.softwareUpdateState = new SoftwareUpdateState();
+ Bridge mockBridge = mock(Bridge.class);
+ when(mockBridge.getLabel()).thenReturn("TestLabel");
+ BridgeHandler mockBridgeHandler = mock(BridgeHandler.class);
+ when(mockBridgeHandler.getThing()).thenReturn(mockBridge);
+ when(mockBridgeHandler.getPublicInformation()).thenReturn(publicInformation);
+ Thing mockBridgeThing = mock(Thing.class);
+ when(mockBridgeThing.getHandler()).thenReturn(mockBridgeHandler);
+ when(thingRegistry.getAll()).thenReturn(Collections.singletonList(mockBridgeThing));
+ assertThat(fixture.buildBridgeInfo(),
+ allOf(containsString("Bridge: TestLabel"), containsString("access possible: false"),
+ containsString("SHC Generation: Gen-T"), containsString("IP Address: 1.2.3.4")));
+
+ // two bridges
+ PublicInformation publicInformation2 = new PublicInformation();
+ publicInformation2.shcGeneration = "Gen-U";
+ publicInformation2.shcIpAddress = "11.22.33.44";
+ publicInformation2.softwareUpdateState = new SoftwareUpdateState();
+ Bridge mockBridge2 = mock(Bridge.class);
+ when(mockBridge2.getLabel()).thenReturn("Bridge 2");
+ BridgeHandler mockBridgeHandler2 = mock(BridgeHandler.class);
+ when(mockBridgeHandler2.getThing()).thenReturn(mockBridge2);
+ when(mockBridgeHandler2.getPublicInformation()).thenReturn(publicInformation2);
+ Thing mockBridgeThing2 = mock(Thing.class);
+ when(mockBridgeThing2.getHandler()).thenReturn(mockBridgeHandler2);
+ when(thingRegistry.getAll()).thenReturn(Arrays.asList(mockBridgeThing, mockBridgeThing2));
+ assertThat(fixture.buildBridgeInfo(),
+ allOf(containsString("Bridge: TestLabel"), containsString("access possible: false"),
+ containsString("SHC Generation: Gen-T"), containsString("IP Address: 1.2.3.4"),
+ containsString("Bridge: Bridge 2"), containsString("access possible: false"),
+ containsString("SHC Generation: Gen-U"), containsString("IP Address: 11.22.33.44")));
+ }
+
+ @Test
+ void printDeviceInfo() throws InterruptedException {
+ // no bridge
+ when(thingRegistry.getAll()).thenReturn(Collections.emptyList());
+ assertThat(fixture.buildDeviceInfo(), is(""));
+
+ // One bridge, No device
+ BridgeHandler mockBridgeHandler = mock(BridgeHandler.class);
+ Thing mockBridgeThing = mock(Thing.class);
+ when(mockBridgeThing.getLabel()).thenReturn("TestLabel");
+ when(mockBridgeThing.getHandler()).thenReturn(mockBridgeHandler);
+ when(thingRegistry.getAll()).thenReturn(Collections.singletonList(mockBridgeThing));
+ assertThat(fixture.buildDeviceInfo(), allOf(containsString("thing: TestLabel"), containsString("devices (0)")));
+
+ // One bridge, One UNsupported device
+ Device mockShcDevice = mock(Device.class);
+ mockShcDevice.deviceModel = "";
+ mockShcDevice.deviceServiceIds = Collections.emptyList();
+ when(mockBridgeHandler.getDevices()).thenReturn(List.of(mockShcDevice));
+ assertThat(fixture.buildDeviceInfo(), allOf(containsString("thing: TestLabel"), containsString("devices (1)"),
+ containsString("!UNSUPPORTED!")));
+
+ // One bridge, One supported device
+ mockShcDevice.deviceModel = "TWINGUARD";
+ mockShcDevice.deviceServiceIds = Collections.emptyList();
+ when(mockBridgeHandler.getDevices()).thenReturn(List.of(mockShcDevice));
+ assertThat(fixture.buildDeviceInfo(), allOf(containsString("thing: TestLabel"), containsString("devices (1)"),
+ containsString("TWINGUARD -> twinguard")));
+
+ // One bridge, One supported device with services
+ mockShcDevice.deviceModel = "TWINGUARD";
+ mockShcDevice.deviceServiceIds = List.of("unknownService", "batterylevel");
+ when(mockBridgeHandler.getDevices()).thenReturn(List.of(mockShcDevice));
+ assertThat(fixture.buildDeviceInfo(), allOf(containsString("thing: TestLabel"), containsString("devices (1)"),
+ containsString("TWINGUARD -> twinguard"), containsString("service: unknownService -> !UNSUPPORTED!"),
+ containsString("batterylevel -> batterylevel")));
+ }
+
+ @Test
+ void printBindingInfo() {
+ assertThat(fixture.buildBindingInfo(), containsString("Bosch SHC Binding"));
+ }
+
+ @Test
+ void printSupportedDevices() {
+ assertThat(fixture.buildSupportedDeviceStatus(),
+ allOf(containsString("Supported Devices"), containsString("BBL = boschshc:shutter-control")));
+ }
+
+ @Test
+ void printSupportedServices() {
+ assertThat(fixture.buildSupportedServiceStatus(),
+ allOf(containsString("Supported Services"), containsString("airqualitylevel")));
+ }
+
+ /**
+ * The list of services returned by getAllBoschShcServices() shall match
+ * the implemented services in org.openhab.bindings.boschshc.internal.services.
+ * Because reflection doesn't return all services classes during runtime
+ * this test supports consistency between the lists of services and the implemented services.
+ */
+ @Test
+ void getAllBoschShcServices() throws IOException {
+ List<String> services = Files
+ .walk(Paths.get("src/main/java/org/openhab/binding/boschshc/internal/services").toAbsolutePath(), 1)
+ .filter(Files::isDirectory).map(Path::getFileName).map(Path::toString)
+ // exclude folders which no service implementation
+ .filter(name -> !name.equals("dto")).filter(name -> !name.equals("services")).sorted()
+ .collect(Collectors.toList());
+ assertThat(services, is(fixture.getAllBoschShcServices()));
+ }
+}